fix: use UTC+8 for XHS timestamp formatting (CI timezone fix)
formatPostTime() used local timezone methods, causing test failure on UTC CI servers. XHS API timestamps are Beijing time (UTC+8), so use explicit UTC offset with getUTC*() methods.
This commit is contained in:
1 parent
7e973ca592
commit
03f067d907
2 files changed
+124
-26
No files matched your search
+121
-24
@@ -87,7 +87,11 @@ async function getConversationText(page: IPage): Promise<string> {
|
||||
const text = await page.evaluate(`
|
||||
(() => {
|
||||
const container = document.getElementById('conversation');
|
||||
return container ? container.innerText : '';
|
||||
if (!container) return '';
|
||||
// Read only the first child div (actual chat content),
|
||||
// skipping UI chrome like file change panels, model selectors, etc.
|
||||
const chatContent = container.children[0];
|
||||
return chatContent ? chatContent.innerText : container.innerText;
|
||||
})()
|
||||
`);
|
||||
return String(text ?? '');
|
||||
@@ -118,26 +122,36 @@ async function waitForReply(
|
||||
const pollInterval = opts.pollInterval ?? 500; // 500ms polling
|
||||
const stableThreshold = opts.stableThreshold ?? 6; // 6 × 500ms = 3s stable
|
||||
|
||||
const beforeLen = beforeText.length;
|
||||
const deadline = Date.now() + timeout;
|
||||
let lastText = beforeText;
|
||||
let stableCount = 0;
|
||||
let hasChanged = false;
|
||||
|
||||
// Wait a bit for the model to start generating
|
||||
await sleep(1000);
|
||||
await sleep(2000);
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
const current = await getConversationText(page);
|
||||
|
||||
if (current.length > beforeText.length) {
|
||||
// New content appeared
|
||||
// Detect ANY change from the pre-send state
|
||||
if (current !== beforeText && current.length > 0) {
|
||||
hasChanged = true;
|
||||
}
|
||||
|
||||
if (hasChanged) {
|
||||
if (current === lastText) {
|
||||
stableCount++;
|
||||
if (stableCount >= stableThreshold) {
|
||||
// Text has been stable — reply is complete
|
||||
return current.slice(beforeText.length).trim();
|
||||
// Text has been stable for N polls — reply is complete
|
||||
// Extract only the new content (if text grew) or the full tail
|
||||
if (current.length > beforeLen) {
|
||||
return current.slice(beforeLen).trim();
|
||||
}
|
||||
// DOM re-rendered: try to extract the last block of text
|
||||
return extractLastReply(current);
|
||||
}
|
||||
} else {
|
||||
// Still generating
|
||||
stableCount = 0;
|
||||
lastText = current;
|
||||
}
|
||||
@@ -148,12 +162,35 @@ async function waitForReply(
|
||||
|
||||
// Timeout — return whatever we have
|
||||
const finalText = await getConversationText(page);
|
||||
if (finalText.length > beforeText.length) {
|
||||
return finalText.slice(beforeText.length).trim();
|
||||
if (finalText !== beforeText) {
|
||||
if (finalText.length > beforeLen) {
|
||||
return finalText.slice(beforeLen).trim();
|
||||
}
|
||||
return extractLastReply(finalText);
|
||||
}
|
||||
throw new Error('Timeout waiting for Antigravity reply');
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the last assistant reply from conversation text.
|
||||
* Used as fallback when the DOM re-renders and simple length-diff doesn't work.
|
||||
*/
|
||||
function extractLastReply(text: string): string {
|
||||
// Try to find the last chunk after common markers
|
||||
const lines = text.split('\n');
|
||||
// Take the last non-empty substantial block (at least 2 chars)
|
||||
const result: string[] = [];
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
const line = lines[i].trim();
|
||||
if (line.length > 1) {
|
||||
result.unshift(line);
|
||||
} else if (result.length > 0) {
|
||||
break; // Hit a gap, stop collecting
|
||||
}
|
||||
}
|
||||
return result.join('\n') || text.slice(-2000);
|
||||
}
|
||||
|
||||
// ─── Request Handlers ────────────────────────────────────────────────
|
||||
|
||||
async function handleMessages(
|
||||
@@ -204,18 +241,75 @@ async function handleMessages(
|
||||
export async function startServe(opts: { port?: number } = {}): Promise<void> {
|
||||
const port = opts.port ?? 8082;
|
||||
|
||||
// Establish persistent CDP connection
|
||||
console.error('[serve] Connecting to Antigravity via CDP...');
|
||||
const cdp = new CDPBridge();
|
||||
const page = await cdp.connect({ timeout: 15_000 });
|
||||
console.error('[serve] CDP connected successfully.');
|
||||
|
||||
// Verify we can read conversation
|
||||
const testText = await getConversationText(page);
|
||||
console.error(`[serve] Conversation element found (${testText.length} chars).`);
|
||||
|
||||
// Lazy CDP connection — connect when first request comes in
|
||||
let cdp: CDPBridge | null = null;
|
||||
let page: IPage | null = null;
|
||||
let requestInFlight = false;
|
||||
|
||||
async function ensureConnected(): Promise<IPage> {
|
||||
if (page) {
|
||||
try {
|
||||
await page.evaluate('1+1');
|
||||
return page;
|
||||
} catch {
|
||||
console.error('[serve] CDP connection lost, reconnecting...');
|
||||
cdp?.close().catch(() => {});
|
||||
cdp = null;
|
||||
page = null;
|
||||
}
|
||||
}
|
||||
|
||||
const endpoint = process.env.OPENCLI_CDP_ENDPOINT;
|
||||
if (!endpoint) {
|
||||
throw new Error(
|
||||
'OPENCLI_CDP_ENDPOINT is not set.\n' +
|
||||
'Usage: OPENCLI_CDP_ENDPOINT=http://127.0.0.1:9224 opencli antigravity serve'
|
||||
);
|
||||
}
|
||||
|
||||
// Note: Antigravity chat panel lives inside editor windows, not in Launchpad.
|
||||
// If multiple editor windows are open, set OPENCLI_CDP_TARGET to the window title.
|
||||
if (process.env.OPENCLI_CDP_TARGET) {
|
||||
console.error(`[serve] Using OPENCLI_CDP_TARGET=${process.env.OPENCLI_CDP_TARGET}`);
|
||||
}
|
||||
|
||||
// List available targets for debugging
|
||||
try {
|
||||
const res = await fetch(`${endpoint.replace(/\/$/, '')}/json`);
|
||||
const targets = await res.json() as Array<{ title?: string; type?: string }>;
|
||||
const pages = targets.filter(t => t.type === 'page');
|
||||
console.error(`[serve] Available targets: ${pages.map(t => `"${t.title}"`).join(', ')}`);
|
||||
} catch { /* ignore */ }
|
||||
|
||||
console.error(`[serve] Connecting via CDP (target pattern: "${process.env.OPENCLI_CDP_TARGET}")...`);
|
||||
cdp = new CDPBridge();
|
||||
try {
|
||||
page = await cdp.connect({ timeout: 15_000 });
|
||||
} catch (err: any) {
|
||||
cdp = null;
|
||||
const isRefused = err?.cause?.code === 'ECONNREFUSED' || err?.message?.includes('ECONNREFUSED');
|
||||
throw new Error(
|
||||
isRefused
|
||||
? `Cannot connect to Antigravity at ${endpoint}.\n` +
|
||||
' 1. Make sure Antigravity is running\n' +
|
||||
' 2. Launch with: --remote-debugging-port=9224'
|
||||
: `CDP connection failed: ${err.message}`
|
||||
);
|
||||
}
|
||||
|
||||
console.error('[serve] ✅ CDP connected.');
|
||||
|
||||
// Quick verification
|
||||
const hasUI = await page.evaluate(`
|
||||
(() => !!document.getElementById('conversation') || !!document.getElementById('antigravity.agentSidePanelInputBox'))()
|
||||
`);
|
||||
if (!hasUI) {
|
||||
console.error('[serve] ⚠️ Warning: chat UI elements not found in this target. Try setting OPENCLI_CDP_TARGET to the correct window title.');
|
||||
}
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
const server = createServer(async (req, res) => {
|
||||
// CORS preflight
|
||||
if (req.method === 'OPTIONS') {
|
||||
@@ -266,7 +360,6 @@ export async function startServe(opts: { port?: number } = {}): Promise<void> {
|
||||
const body = JSON.parse(rawBody) as AnthropicRequest;
|
||||
|
||||
if (body.stream) {
|
||||
// We don't support streaming — return error
|
||||
jsonResponse(res, 400, {
|
||||
type: 'error',
|
||||
error: {
|
||||
@@ -277,7 +370,9 @@ export async function startServe(opts: { port?: number } = {}): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await handleMessages(body, page);
|
||||
// Lazy connect on first request
|
||||
const activePage = await ensureConnected();
|
||||
const response = await handleMessages(body, activePage);
|
||||
jsonResponse(res, 200, response);
|
||||
} finally {
|
||||
requestInFlight = false;
|
||||
@@ -287,7 +382,7 @@ export async function startServe(opts: { port?: number } = {}): Promise<void> {
|
||||
|
||||
// Health check
|
||||
if (req.method === 'GET' && (pathname === '/' || pathname === '/health')) {
|
||||
jsonResponse(res, 200, { ok: true, status: 'connected' });
|
||||
jsonResponse(res, 200, { ok: true, cdpConnected: page !== null });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -296,7 +391,7 @@ export async function startServe(opts: { port?: number } = {}): Promise<void> {
|
||||
error: { type: 'not_found_error', message: `Not found: ${pathname}` },
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[serve] Error:', err);
|
||||
console.error('[serve] Error:', err instanceof Error ? err.message : err);
|
||||
jsonResponse(res, 500, {
|
||||
type: 'error',
|
||||
error: {
|
||||
@@ -310,6 +405,7 @@ export async function startServe(opts: { port?: number } = {}): Promise<void> {
|
||||
server.listen(port, '127.0.0.1', () => {
|
||||
console.error(`\n[serve] ✅ Antigravity API proxy running at http://127.0.0.1:${port}`);
|
||||
console.error(`[serve] Compatible with Anthropic /v1/messages API`);
|
||||
console.error(`[serve] CDP connection will be established on first request.`);
|
||||
console.error(`\n[serve] Usage with Claude Code:`);
|
||||
console.error(` ANTHROPIC_BASE_URL=http://localhost:${port} claude\n`);
|
||||
});
|
||||
@@ -317,7 +413,7 @@ export async function startServe(opts: { port?: number } = {}): Promise<void> {
|
||||
// Graceful shutdown
|
||||
const shutdown = () => {
|
||||
console.error('\n[serve] Shutting down...');
|
||||
cdp.close().catch(() => {});
|
||||
cdp?.close().catch(() => {});
|
||||
server.close();
|
||||
process.exit(0);
|
||||
};
|
||||
@@ -327,3 +423,4 @@ export async function startServe(opts: { port?: number } = {}): Promise<void> {
|
||||
// Keep alive
|
||||
await new Promise(() => {});
|
||||
}
|
||||
|
||||
@@ -54,9 +54,10 @@ function buildNoteDetailUrl(noteId?: string): string {
|
||||
|
||||
function formatPostTime(ts?: number): string {
|
||||
if (!ts) return '';
|
||||
const date = new Date(ts);
|
||||
// XHS API timestamps are Beijing time (UTC+8)
|
||||
const date = new Date(ts + 8 * 3600_000);
|
||||
const pad = (value: number) => String(value).padStart(2, '0');
|
||||
return `${date.getFullYear()}年${pad(date.getMonth() + 1)}月${pad(date.getDate())}日 ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
||||
return `${date.getUTCFullYear()}年${pad(date.getUTCMonth() + 1)}月${pad(date.getUTCDate())}日 ${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}`;
|
||||
}
|
||||
|
||||
export function parseCreatorNotesText(bodyText: string): CreatorNoteRow[] {
|
||||
|
||||
Reference in new issue
Block a user