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

159 lines
7.4 KiB
JavaScript

import { cli, Strategy } from '@jackwener/opencli/registry';
import { CliError, CommandExecutionError, EXIT_CODES, TimeoutError } from '@jackwener/opencli/errors';
import {
DEEPSEEK_DOMAIN, DEEPSEEK_URL, ensureOnDeepSeek, selectModel, setFeature,
sendMessage, sendWithFile, getBubbleCount, waitForResponse, parseBoolFlag, withRetry,
pickResumeUrl, TEXTAREA_SELECTOR,
} from './utils.js';
export const askCommand = cli({
site: 'deepseek',
name: 'ask',
access: 'write',
description: 'Send a prompt to DeepSeek and get the response',
domain: DEEPSEEK_DOMAIN,
strategy: Strategy.COOKIE,
browser: true,
siteSession: 'persistent',
navigateBefore: false,
args: [
{ name: 'prompt', positional: true, required: true, help: 'Prompt to send' },
{ name: 'timeout', type: 'int', default: 120, help: 'Max seconds to wait for response' },
{ name: 'new', type: 'boolean', default: false, help: 'Start a new chat before sending' },
{ name: 'model', default: 'instant', choices: ['instant', 'expert', 'vision'], help: 'Model to use: instant, expert, or vision' },
{ name: 'think', type: 'boolean', default: false, help: 'Enable DeepThink mode' },
{ name: 'search', type: 'boolean', default: false, help: 'Enable web search' },
{ name: 'file', help: 'Attach a file (PDF, image, text) with the prompt' },
],
// columns omitted: derived from row keys so non-think output shows only 'response'
func: async (page, kwargs) => {
const prompt = kwargs.prompt;
const timeoutMs = (kwargs.timeout || 120) * 1000;
const wantThink = parseBoolFlag(kwargs.think);
const wantSearch = parseBoolFlag(kwargs.search);
const wantModel = kwargs.model || 'instant';
if ((wantModel === 'vision' || wantModel === 'expert') && wantSearch) {
throw new CliError(
'ARGUMENT',
`DeepSeek ${wantModel} mode does not support --search.`,
'Run without --search, or use --model instant for web search.',
EXIT_CODES.USAGE_ERROR,
);
}
if (parseBoolFlag(kwargs.new)) {
await page.goto(DEEPSEEK_URL);
// Wait for the composer to mount instead of a fixed 3 s sleep.
try {
await page.wait({ selector: TEXTAREA_SELECTOR, timeout: 8 });
} catch {
// Selector still missing → downstream selectModel/sendMessage
// will surface the failure with a typed error.
}
} else {
const navigated = await ensureOnDeepSeek(page);
if (navigated) {
// Pinned conversations sit in their own DOM section and are
// skipped so the resume never lands on a topped chat.
const resumeUrl = await pickResumeUrl(page);
if (!resumeUrl) {
throw new CommandExecutionError(
'Workspace was recycled but no prior conversation could be loaded',
'Pass --new to start a fresh chat, or wait for the sidebar to populate before retrying.',
);
}
await page.goto(resumeUrl);
try {
await page.wait({ selector: TEXTAREA_SELECTOR, timeout: 5 });
} catch {
// Conversation page may still be loading; subsequent steps
// will retry or report.
}
}
}
// Model selector is only available on the new-chat page, not inside
// an existing conversation. Skip it when we resumed a prior thread.
const currentUrl = await page.evaluate('window.location.href') || '';
const inConversation = currentUrl.includes('/a/chat/s/');
const modelExplicit = kwargs.__opencliOptionSources?.model === 'cli';
if (inConversation && modelExplicit) {
throw new CliError(
'ARGUMENT',
`Cannot switch to ${wantModel} model inside an existing conversation.`,
'Re-run with --new to start a fresh chat before selecting a model.',
EXIT_CODES.USAGE_ERROR,
);
}
if (!inConversation) {
const modelResult = await withRetry(() => selectModel(page, wantModel));
if (!modelResult?.ok) {
throw new CommandExecutionError(`Could not switch to ${wantModel} model`);
}
// The 0.5 s settle previously here was redundant: each subsequent
// step (setFeature, sendMessage) issues a fresh CDP eval, giving
// React more than enough time to flush the toggle's state update.
}
const thinkResult = await withRetry(() => setFeature(page, 'DeepThink', wantThink));
if (!thinkResult?.ok && wantThink) {
throw new CommandExecutionError('Could not enable DeepThink');
}
// Only instant mode has the Search toggle in the DeepSeek UI.
let searchResult;
if (wantModel !== 'vision' && wantModel !== 'expert') {
searchResult = await withRetry(() => setFeature(page, 'Search', wantSearch));
if (!searchResult?.ok && wantSearch) {
throw new CommandExecutionError('Could not enable Search');
}
}
// No settle wait after toggles: the next CDP eval below already gives
// React time to flush the aria-checked state.
if (kwargs.file) {
const baseline = await withRetry(() => getBubbleCount(page));
try {
const fileResult = await sendWithFile(page, kwargs.file, prompt);
if (fileResult && !fileResult.ok) {
throw new CommandExecutionError(fileResult.reason || 'Failed to attach file');
}
} catch (err) {
// SPA navigates after send; "Promise was collected" means send succeeded
if (!String(err?.message || err).includes('Promise was collected')) throw err;
}
// waitForResponse polls every 3 s for new bubbles, so the previous
// 3 s settle here was a redundant sleep on top of the first poll.
const result = await waitForResponse(page, baseline, prompt, timeoutMs, wantThink);
if (!result) {
throw new TimeoutError('deepseek ask', kwargs.timeout, 'No DeepSeek reply observed before timeout. Retry with --timeout increased.');
}
if (wantThink && typeof result === 'object' && result.response !== undefined) {
return [result];
}
return [{ response: result }];
}
const baseline = await withRetry(() => getBubbleCount(page));
const sendResult = await withRetry(() => sendMessage(page, prompt));
if (!sendResult?.ok) {
throw new CommandExecutionError(sendResult?.reason || 'Failed to send message');
}
const result = await waitForResponse(page, baseline, prompt, timeoutMs, wantThink);
if (!result) {
throw new TimeoutError('deepseek ask', kwargs.timeout, 'No DeepSeek reply observed before timeout. Retry with --timeout increased.');
}
if (wantThink && typeof result === 'object' && result.response !== undefined) {
return [result];
}
return [{ response: result }];
},
});