自动为外部网络服务注册——浏览器自动化、邮件验证、API密钥生成以及在1Password中安全存储。在被要求c...时使用。
Web 服务登机技能是一项面向实际任务的技能,主要用于使用单个未断开的浏览器会话进行;绝不关闭步间浏览器;
该技能适合需要稳定复用相关能力的场景,可作为自动化工作流的一部分,也便于后续检查、调整和扩展。从功能定位来看,该技能强调把分散的操作要求整理成清晰、可复用的处理流程,使用户能够围绕既定目标快速准备输入、选择执行方式并获得结构化结果。实际使用前应先确认任务范围、数据来源、运行环境、必要权限和关键参数,再依据技能说明逐步执行;
若输入条件不完整,应先补齐信息或采用保守配置,避免因错误假设导致结果偏离需求。执行过程中需要关注工具调用是否成功、接口或依赖是否可用、输出格式是否符合预期,并对异常提示、缺失字段和边界情况进行处理;涉及批量任务时,还应保存进度,避免中断后重复操作。
发送注册邮件 → 验证邮箱 → 完成注册 → 生成 API 密钥 → 安全存入 1Password → 注入 .env 文件
请在单个不间断的浏览器会话中完成全部步骤。步骤之间切勿关闭浏览器。
user+alias@domain.com 视为与 user@domain.com 同一用户user@domain.com 账户,所有别名邮件均路由至该已有账户WebAuthn.addVirtualAuthenticator 创建的是内存级凭据存储const creds = await cdp.send('WebAuthn.getCredentials', { authenticatorId });
fs.writeFileSync('/tmp/webauthn-creds.json', JSON.stringify(creds));
const saved = JSON.parse(fs.readFileSync('/tmp/webauthn-creds.json'));
for (const cred of saved.credentials) {
await cdp.send('WebAuthn.addCredential', { authenticatorId, credential: cred });
}
=n)app.service.com/internal/api/* 接口依赖会话 Cookie 认证api.service.com/public/v1/* 接口要求 API 密钥签名(X-Stamp)const ctxClean = await browser.newContext({ storageState: undefined });
const page = await ctxClean.newPage();
await page.goto('https://service.com/signup');
// 填写邮箱,点击继续
// 提交后立即关闭 ctxClean
await ctxClean.close();
为何分离? 防止既有会话 Cookie 干扰注册流程。
const ctx = await browser.newContext({ storageState: undefined });
const page = await ctx.newPage();
const cdp = await ctx.newCDPSession(page);
// 在导航前配置虚拟认证器
await cdp.send('WebAuthn.enable', { enableUI: false });
const { authenticatorId } = await cdp.send('WebAuthn.addVirtualAuthenticator', {
options: {
protocol: 'ctap2', transport: 'internal',
hasResidentKey: true, hasUserVerification: true,
isUserVerified: true, automaticPresenceSimulation: true,
}
});
// 导航至验证链接
await page.goto(verifyUrl);
// 完成注册后续步骤...
// 通行密钥注册完成后立即导出
const creds = await cdp.send('WebAuthn.getCredentials', { authenticatorId });
fs.writeFileSync('/tmp/webauthn-creds.json', JSON.stringify(creds));
console.log('凭据已备份:', creds.credentials?.length);
// 在同一会话中继续完成 API 密钥与钱包创建
// ...保存 API 密钥...
// 仅当所有凭证均已存入 1Password 后,才关闭浏览器
function fetchLatestTurnkeyLink(host='127.0.0.1', port=1143, user, pass) {
return new Promise((resolve) => {
const socket = net.connect(port, host);
let buf='', tls2=null, step=0, body=[], inBody=false;
const t = setTimeout(() => { try{(tls2||socket).destroy()}catch(e){}; resolve(null); }, 22000);
function send(cmd) { (tls2||socket).write(cmd+'\r\n'); }
function onData(data) {
buf += data.toString();
const lines = buf.split('\r\n'); buf = lines.pop();
for (const l of lines) {
if (inBody) body.push(l);
if (step===0 && l.includes('OK')) { step=1; send('a1 STARTTLS'); }
else if (step===1 && l.includes('a1 OK')) { tls2=tls.connect({socket,rejectUnauthorized:false}); tls2.on('data',onData); step=2; send(`a2 LOGIN "${user}" "${pass}"`); }
else if (step===2 && l.includes('a2 OK')) { step=3; send('a3 SELECT INBOX'); }
else if (step===3 && l.includes('a3 OK')) { step=4; send('a4 SEARCH ALL'); }
else if (step===4 && l.startsWith('* SEARCH')) {
const nums = l.replace('* SEARCH','').trim().split(' ').filter(Boolean);
step=5; inBody=true; send(`a5 FETCH ${nums[nums.length-1]} (BODY[TEXT])`);
}
else if (step===5 && l.includes('a5 OK')) {
clearTimeout(t); (tls2||socket).end();
// 解码 quoted-printable
const decoded = body.join('\n')
.replace(/=\r?\n/g, '')
.replace(/=([0-9A-Fa-f]{2})/g, (_, h) => String.fromCharCode(parseInt(h, 16)));
// 提取重定向 URL
const urls = [...decoded.matchAll(/https:\/\/service\.com\/redirect\?token=[^\s"<>)]+/g)].map(m=>m[0]);
resolve(urls[0] || null);
}
}
}
socket.on('data', onData);
socket.on('error', () => { clearTimeout(t); resolve(null); });
});
}
要点: 应匹配服务所属域名的重定向 URL,而非泛化 URL 模式。
127.0.0.1,端口:1143,启用 STARTTLSop://OpenClaw/Proton Bridge - Monk Fenix/...ps aux | grep -i bridge标准 element.fill() 在 React 输入框上偶有失效。请改用以下方式:
await page.evaluate((value) => {
const input = document.querySelector('input');
Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')
.set.call(input, value);
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
}, value);
位于视口外的按钮点击会报错 element is outside of the viewport。务必先滚动:
await page.evaluate((text) => {
const btn = [...document.querySelectorAll('button')]
.find(b => b.textContent?.toLowerCase().includes(text) && !b.disabled);
if (btn) { btn.scrollIntoView(); btn.click(); }
}, buttonText);
针对提供内部浏览器 API 的服务:
// 从页面上下文调用已认证的内部 API
const data = await page.evaluate(async () => {
const r = await fetch('/internal/api/v1/whoami');
return r.json();
});
// data.organizationId, data.userId 等字段
完成认证(Cookie 已存在)后,可在页面上下文中调用内部接口:
const result = await page.evaluate(async ({ orgId, publicKey }) => {
const r = await fetch('/tkhq/api/v1/activities', { // 根据具体服务调整路径
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: 'ACTIVITY_TYPE_CREATE_API_KEYS',
organizationId: orgId,
parameters: { apiKeys: [{ apiKeyName: 'my-key', publicKey, curveType: 'API_KEY_CURVE_P256' }] }
})
});
return { status: r.status, body: await r.text() };
}, { orgId, publicKey });
重要提示: 各服务内部接口路径各不相同。创建资源前,请捕获网络请求以确认真实接口:
page.on('request', req => {
if (req.method() !== 'GET' && req.url().includes(serviceDomain))
console.log(req.method(), req.url());
});
op item create --vault OpenClaw --title "Turnkey API Credentials — Reddi Agent Protocol" --category "API Credential" "org_id[text]=$ORG_ID" "user_id[text]=$USER_ID" "api_public_key[text]=$API_PUB" "api_private_key[password]=$API_PRIV" "wallet_id[text]=$WALLET_ID" "wallet_address[text]=$WALLET_ADDR"
启动任何注册流程前,必须向 Notion 注册登记表添加一条记录:
322eb552-581a-81dc-adbc-fabb7af1d311app.turnkey.com)/tkhq/api/v1/activities(内部接口,Cookie 认证)api.turnkey.com/public/v1/ 需 X-Stamp(已签名请求)+tag)映射至同一 Turnkey 账户——如需独立组织,请更换邮箱服务商b7378687-cf82-45ab-a46c-7dda9239001d(Reddi Agent Protocol)启动任意注册流程前,请确认以下事项:
相关专题
热门下载
相关下载
精品课程
共6课时 | 54.6万人学习
共89课时 | 133.4万人学习
共49课时 | 82.2万人学习