Files
wehub-resource-sync 9b395f5cc3
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
Build Chrome Extension / build (push) Has been cancelled
Trigger Website Rebuild (Docs Updated) / dispatch (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:48 +08:00

363 lines
14 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { cli, Strategy } from '@jackwener/opencli/registry';
import { htmlToMarkdown } from '@jackwener/opencli/utils';
import { ArgumentError, CommandExecutionError, TimeoutError } from '@jackwener/opencli/errors';
import {
YUANBAO_DOMAIN,
IS_VISIBLE_JS,
authRequired,
isOnYuanbao,
ensureYuanbaoPage,
hasLoginGate,
normalizeBooleanFlag,
sendYuanbaoMessage,
} from './shared.js';
const YUANBAO_RESPONSE_POLL_INTERVAL_SECONDS = 2;
const YUANBAO_MIN_WAIT_MS = 8_000;
const YUANBAO_STABLE_POLLS_REQUIRED = 3;
function sendFailure(reason, detail) {
const suffix = detail ? ` Detail: ${detail}` : '';
return new CommandExecutionError(`${reason || 'Unknown Yuanbao send failure.'}${suffix}`, 'Make sure the Yuanbao chat composer is visible and ready before retrying.');
}
function normalizeText(value) {
return typeof value === 'string' ? value.trim() : '';
}
export function convertYuanbaoHtmlToMarkdown(value) {
return htmlToMarkdown(value, (td) => {
td.addRule('table', {
filter: 'table',
replacement: (content) => `\n\n${content}\n\n`,
});
td.addRule('tableSection', {
filter: ['thead', 'tbody', 'tfoot'],
replacement: (content) => content,
});
td.addRule('tableRow', {
filter: 'tr',
replacement: (content, node) => {
const element = node;
const cells = Array.from(element.children);
const isHeaderRow = element.parentElement?.tagName === 'THEAD'
|| (cells.length > 0 && cells.every((cell) => cell.tagName === 'TH'));
const row = `${content}\n`;
if (!isHeaderRow)
return row;
const separator = `| ${cells.map(() => '---').join(' | ')} |\n`;
return `${row}${separator}`;
},
});
td.addRule('tableCell', {
filter: ['th', 'td'],
replacement: (content, node) => {
const element = node;
const index = element.parentElement ? Array.from(element.parentElement.children).indexOf(element) : 0;
const prefix = index === 0 ? '| ' : ' ';
return `${prefix}${content.trim()} |`;
},
});
});
}
export function sanitizeYuanbaoResponseText(value, promptText) {
let sanitized = value
.replace(/内容由AI生成,仅供参考/gi, '')
.replace(/重新回答/gi, '')
.trim();
if (/^(正在搜索资料|搜索资料中|正在思考|思考中)[.。…]*$/u.test(sanitized)) {
return '';
}
const prompt = promptText.trim();
if (!prompt)
return sanitized;
if (sanitized === prompt)
return '';
for (const separator of ['\n\n', '\n', '\r\n\r\n', '\r\n', ' ']) {
const prefix = `${prompt}${separator}`;
if (sanitized.startsWith(prefix)) {
sanitized = sanitized.slice(prefix.length).trim();
break;
}
}
return sanitized;
}
export function collectYuanbaoTranscriptAdditions(beforeLines, currentLines, promptText) {
const beforeSet = new Set(beforeLines);
const additions = currentLines
.filter((line) => !beforeSet.has(line))
.map((line) => sanitizeYuanbaoResponseText(line, promptText))
.filter((line) => line && line !== promptText);
return additions.join('\n').trim();
}
export function pickLatestYuanbaoAssistantCandidate(messages, baselineCount, promptText) {
const freshMessages = messages
.slice(Math.max(0, baselineCount))
.map((message) => sanitizeYuanbaoResponseText(message, promptText))
.filter(Boolean);
for (let i = freshMessages.length - 1; i >= 0; i -= 1) {
if (freshMessages[i] !== promptText.trim())
return freshMessages[i];
}
return '';
}
export function updateStableState(previousText, stableCount, nextText) {
if (!nextText)
return { previousText: '', stableCount: 0 };
if (nextText === previousText)
return { previousText, stableCount: stableCount + 1 };
return { previousText: nextText, stableCount: 0 };
}
function getTranscriptLinesScript() {
return `
(() => {
const clean = (value) => (value || '')
.replace(/\\u00a0/g, ' ')
.replace(/\\n{3,}/g, '\\n\\n')
.trim();
const root = (
document.querySelector('.agent-dialogue__content--common')
|| document.querySelector('.agent-dialogue__content')
|| document.querySelector('.agent-dialogue')
|| document.body
).cloneNode(true);
const removableSelectors = [
'.agent-dialogue__content--common__input',
'.agent-dialogue__tool',
'.agent-dialogue__content-copyright',
'.index_chatLandingBox__G7hAT',
'.index_chatLandingBoxMobile__J8i8v',
'.index_chatLandingHintList__M69Lr',
'.yb-nav',
'.agent-dialogue__content--common__input .ql-toolbar',
'.agent-dialogue__content--common__input .ql-container',
'.agent-dialogue__content--common__input .ql-editor',
'[role="dialog"]',
'iframe',
'button',
'script',
'style',
'noscript',
];
for (const selector of removableSelectors) {
root.querySelectorAll(selector).forEach((node) => node.remove());
}
const stopLines = new Set([
'元宝',
'DeepSeek',
'深度思考',
'联网搜索',
'工具',
'登录',
'安装电脑版',
'内容由AI生成,仅供参考',
'有问题,尽管问,shift+enter换行',
'立即创建团队',
'微信',
'手机',
'QQ',
'微信扫码登录',
'扫码默认已阅读并同意',
'用户服务协议',
'隐私协议',
]);
const noisyPatterns = [
/^支持文件格式[:]/,
/^文件拖动到此处即可上传/,
/^下载元宝电脑版/,
];
return clean(root.innerText || root.textContent || '')
.split('\\n')
.map((line) => clean(line))
.filter((line) => line
&& line.length <= 4000
&& !stopLines.has(line)
&& !noisyPatterns.some((pattern) => pattern.test(line)));
})()
`;
}
async function getYuanbaoTranscriptLines(page) {
const result = await page.evaluate(getTranscriptLinesScript());
return Array.isArray(result) ? result.map(normalizeText).filter(Boolean) : [];
}
async function getYuanbaoAssistantMessages(page) {
const result = await page.evaluate(`(() => {
${IS_VISIBLE_JS}
const roots = Array.from(document.querySelectorAll('.agent-chat__list__item--ai'))
.filter((node) => isVisible(node));
return roots.map((root) => {
const doneContent = root.querySelector('.hyc-content-md-done');
const markdownContent = doneContent || root.querySelector('.hyc-content-md');
const speechContent = root.querySelector('.agent-chat__speech-text');
const bubbleContent = root.querySelector('.agent-chat__bubble__content');
const content = markdownContent || speechContent || bubbleContent;
if (content instanceof HTMLElement) {
return content.innerHTML || content.textContent || '';
}
return root instanceof HTMLElement ? (root.innerHTML || root.textContent || '') : '';
}).filter(Boolean);
})()`);
return Array.isArray(result)
? result
.map((value) => convertYuanbaoHtmlToMarkdown(typeof value === 'string' ? value : ''))
.map(normalizeText)
.filter(Boolean)
: [];
}
async function getYuanbaoInternetSearchState(page) {
const result = await page.evaluate(`(() => {
${IS_VISIBLE_JS}
const button = Array.from(document.querySelectorAll('[dt-button-id="internet_search"]'))
.find((node) => isVisible(node));
if (!(button instanceof HTMLElement)) return { found: false, enabled: false };
const attr = button.getAttribute('dt-internet-search') || '';
const className = button.className || '';
return {
found: true,
enabled: attr === 'openInternetSearch' || className.includes('index_v2_active__'),
};
})()`);
return result;
}
async function setYuanbaoInternetSearch(page, enabled) {
const current = await getYuanbaoInternetSearchState(page);
if (!current.found || current.enabled === enabled)
return;
await page.evaluate(`(() => {
${IS_VISIBLE_JS}
const button = Array.from(document.querySelectorAll('[dt-button-id="internet_search"]'))
.find((node) => isVisible(node));
if (button instanceof HTMLElement) button.click();
})()`);
await page.wait(0.5);
}
async function getYuanbaoDeepThinkState(page) {
const result = await page.evaluate(`(() => {
${IS_VISIBLE_JS}
const button = Array.from(document.querySelectorAll('[dt-button-id="deep_think"]'))
.find((node) => isVisible(node));
if (!(button instanceof HTMLElement)) return { found: false, enabled: false };
const className = button.className || '';
return {
found: true,
enabled: className.includes('ThinkSelector_selected__'),
};
})()`);
return result;
}
async function setYuanbaoDeepThink(page, enabled) {
const current = await getYuanbaoDeepThinkState(page);
if (!current.found || current.enabled === enabled)
return;
await page.evaluate(`(() => {
${IS_VISIBLE_JS}
const button = Array.from(document.querySelectorAll('[dt-button-id="deep_think"]'))
.find((node) => isVisible(node));
if (button instanceof HTMLElement) button.click();
})()`);
await page.wait(0.5);
}
async function waitForYuanbaoResponse(page, baselineAssistantCount, beforeLines, prompt, timeoutSeconds) {
const startTime = Date.now();
let previousText = '';
let stableCount = 0;
let latestCandidate = '';
while (Date.now() - startTime < timeoutSeconds * 1000) {
await page.wait(YUANBAO_RESPONSE_POLL_INTERVAL_SECONDS);
if (await hasLoginGate(page))
return 'blocked';
const assistantMessages = await getYuanbaoAssistantMessages(page);
const assistantCandidate = pickLatestYuanbaoAssistantCandidate(assistantMessages, baselineAssistantCount, prompt);
const candidate = assistantCandidate || collectYuanbaoTranscriptAdditions(beforeLines, await getYuanbaoTranscriptLines(page), prompt);
if (!candidate)
continue;
latestCandidate = candidate;
const nextState = updateStableState(previousText, stableCount, candidate);
previousText = nextState.previousText;
stableCount = nextState.stableCount;
const waitedLongEnough = Date.now() - startTime >= YUANBAO_MIN_WAIT_MS;
if (waitedLongEnough && stableCount >= YUANBAO_STABLE_POLLS_REQUIRED)
return candidate;
}
return latestCandidate || null;
}
export const askCommand = cli({
site: 'yuanbao',
name: 'ask',
access: 'write',
description: 'Send a prompt to Yuanbao web chat and wait for the assistant response',
domain: YUANBAO_DOMAIN,
strategy: Strategy.COOKIE,
browser: true,
siteSession: 'persistent',
navigateBefore: false,
defaultFormat: 'plain',
args: [
{ name: 'prompt', required: true, positional: true, help: 'Prompt to send' },
{ name: 'timeout', type: 'int', required: false, help: 'Max seconds to wait (default: 60)', default: 60 },
{ name: 'search', type: 'boolean', required: false, help: 'Enable Yuanbao internet search (default: true)', default: true },
{ name: 'think', type: 'boolean', required: false, help: 'Enable Yuanbao deep thinking (default: false)', default: false },
],
columns: ['Role', 'Text'],
func: async (page, kwargs) => {
const prompt = kwargs.prompt;
const timeout = kwargs.timeout;
if (!Number.isInteger(timeout) || timeout < 1) {
throw new ArgumentError('--timeout must be a positive integer (seconds)');
}
const useSearch = normalizeBooleanFlag(kwargs.search, true);
const useThink = normalizeBooleanFlag(kwargs.think, false);
await ensureYuanbaoPage(page);
if (await hasLoginGate(page)) {
throw authRequired('Yuanbao opened a login gate before sending the prompt.');
}
await setYuanbaoInternetSearch(page, useSearch);
await setYuanbaoDeepThink(page, useThink);
const beforeAssistantMessages = await getYuanbaoAssistantMessages(page);
const beforeLines = await getYuanbaoTranscriptLines(page);
const sendResult = await sendYuanbaoMessage(page, prompt);
if (!sendResult?.ok) {
if (await hasLoginGate(page)) {
throw authRequired('Yuanbao opened a login gate instead of accepting the prompt.');
}
throw sendFailure(sendResult?.reason, sendResult?.detail);
}
const response = await waitForYuanbaoResponse(page, beforeAssistantMessages.length, beforeLines, prompt, timeout);
if (response === 'blocked') {
throw authRequired('Yuanbao opened a login gate instead of returning a chat response.');
}
if (!response) {
throw new TimeoutError('yuanbao ask', timeout, 'No Yuanbao response was observed before the timeout. Retry with --timeout, and verify the current browser session is still interactive.');
}
return [
{ Role: 'User', Text: prompt },
{ Role: 'Assistant', Text: response },
];
},
});
export const __test__ = {
collectYuanbaoTranscriptAdditions,
convertYuanbaoHtmlToMarkdown,
isOnYuanbao,
normalizeBooleanFlag,
pickLatestYuanbaoAssistantCandidate,
sanitizeYuanbaoResponseText,
updateStableState,
};