Files
wehub-resource-sync 9b395f5cc3
Build Chrome Extension / build (push) Waiting to run
Trigger Website Rebuild (Docs Updated) / dispatch (push) Waiting to run
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
chore: import upstream snapshot with attribution
2026-07-13 12:39:48 +08:00

148 lines
5.9 KiB
JavaScript

// Quest (= conversation) lifecycle commands: new / send / ask.
import { cli, Strategy } from '@jackwener/opencli/registry';
import {
ArgumentError,
CommandExecutionError,
TimeoutError,
} from '@jackwener/opencli/errors';
import {
buildQoderInjectTextScript,
clickByTextScript,
clickFirstScript,
evaluateQoder,
parsePositiveInt,
QODER_MESSAGE_COUNT_JS,
qoderResponseAfterScript,
} from './_utils.js';
async function waitForMessageCountGrowth(page, beforeCount, timeoutMs = 5000) {
const attempts = Math.max(1, Math.ceil(timeoutMs / 500));
for (let i = 0; i < attempts; i++) {
await page.wait(0.5);
const afterCount = await evaluateQoder(page, QODER_MESSAGE_COUNT_JS);
if (Number(afterCount) > Number(beforeCount)) return afterCount;
}
return beforeCount;
}
// -------- new --------
cli({
site: 'qoder',
name: 'new',
access: 'write',
description: 'Start a new Qoder Quest (conversation). Clicks the "New Quest" button in the sidebar (or its ⌘N variant).',
domain: 'localhost',
strategy: Strategy.UI,
browser: true,
args: [],
columns: ['Status'],
func: async (page) => {
const res = await evaluateQoder(page, clickByTextScript(['New Quest']));
if (!res?.ok) throw new CommandExecutionError(res?.reason || 'New Quest button not found', '');
await page.wait(0.5);
return [{ Status: 'started' }];
},
});
// -------- send --------
cli({
site: 'qoder',
name: 'send',
access: 'write',
description: 'Type text into the Qoder composer and click "Send message" (fire-and-forget).',
domain: 'localhost',
strategy: Strategy.UI,
browser: true,
args: [
{ name: 'text', positional: true, required: true, help: 'Text to send' },
],
columns: ['Status', 'Length'],
func: async (page, kwargs) => {
const text = String(kwargs?.text || '').trim();
if (!text) throw new ArgumentError('text is required');
const beforeCount = await evaluateQoder(page, QODER_MESSAGE_COUNT_JS);
const typeRes = await evaluateQoder(page, buildQoderInjectTextScript(text));
if (!typeRes?.ok) throw new CommandExecutionError(typeRes?.reason || 'composer type failed', '');
await page.wait(0.3);
// Click Send message
const sendRes = await evaluateQoder(page, clickFirstScript([
'button[aria-label="Send message"]',
'button[title="Send message"]',
]));
if (!sendRes?.ok) {
// Fallback: try clickByText.
const textRes = await evaluateQoder(page, clickByTextScript(['Send message', 'Send', '发送']));
if (!textRes?.ok) throw new CommandExecutionError('Send button not found', '');
}
const afterCount = await waitForMessageCountGrowth(page, beforeCount);
if (Number(afterCount) <= Number(beforeCount)) {
throw new CommandExecutionError('Qoder send did not create a new visible message row');
}
return [{ Status: 'sent', Length: String(text.length) }];
},
});
// -------- ask --------
cli({
site: 'qoder',
name: 'ask',
access: 'write',
description: 'Send a prompt to Qoder and wait up to --timeout seconds for the reply (best-effort: polls for the chat turn count to grow + stabilize).',
domain: 'localhost',
strategy: Strategy.UI,
browser: true,
args: [
{ name: 'text', positional: true, required: true, help: 'Prompt text' },
{ name: 'timeout', type: 'int', required: false, default: 120, help: 'Max seconds to wait' },
],
columns: ['Role', 'Text', 'WaitedSeconds'],
func: async (page, kwargs) => {
const text = String(kwargs?.text || '').trim();
if (!text) throw new ArgumentError('text is required');
const timeoutSec = parsePositiveInt(kwargs?.timeout, 120, '--timeout');
// Type + send (mirror `send` logic)
const sendBefore = await evaluateQoder(page, QODER_MESSAGE_COUNT_JS);
const typeRes = await evaluateQoder(page, buildQoderInjectTextScript(text));
if (!typeRes?.ok) throw new CommandExecutionError(typeRes?.reason || 'composer type failed', '');
await page.wait(0.3);
const sendRes = await evaluateQoder(page, clickFirstScript(['button[aria-label="Send message"]', 'button[title="Send message"]']));
if (!sendRes?.ok) {
const textRes = await evaluateQoder(page, clickByTextScript(['Send message', 'Send', '发送']));
if (!textRes?.ok) throw new CommandExecutionError('Send button not found', '');
}
const startedAt = Date.now();
const deadline = startedAt + timeoutSec * 1000;
let lastCount = sendBefore;
let stableTicks = 0;
let response = null;
while (Date.now() < deadline) {
await new Promise((r) => setTimeout(r, 1500));
const cur = await evaluateQoder(page, QODER_MESSAGE_COUNT_JS);
if (cur !== lastCount) {
lastCount = cur;
stableTicks = 0;
} else {
stableTicks++;
}
// Consider stable after 6 idle ticks (≈9s no change) IF count grew at all.
if (lastCount > sendBefore && stableTicks >= 6) {
response = await evaluateQoder(page, qoderResponseAfterScript(sendBefore, text));
if (response?.text) break;
}
}
const elapsed = Math.round((Date.now() - startedAt) / 1000);
if (!response?.text) {
throw new TimeoutError('Qoder response', timeoutSec, 'Confirm Qoder sent the prompt and finished generating, then retry with a larger --timeout.');
}
return [
{ Role: 'User', Text: text, WaitedSeconds: String(elapsed) },
{ Role: response.role || 'Assistant', Text: String(response.text).slice(0, 1200), WaitedSeconds: String(elapsed) },
];
},
});