@jackwener/opencli 1.0.1 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/build-extension.yml +80 -0
- package/.github/workflows/ci.yml +6 -6
- package/.github/workflows/docs.yml +52 -0
- package/.github/workflows/e2e-headed.yml +2 -2
- package/.github/workflows/pkg-pr-new.yml +2 -2
- package/.github/workflows/release.yml +2 -5
- package/.github/workflows/security.yml +2 -2
- package/CDP.md +1 -1
- package/CDP.zh-CN.md +1 -1
- package/README.md +42 -34
- package/README.zh-CN.md +42 -34
- package/SKILL.md +3 -5
- package/dist/browser/cdp.d.ts +42 -0
- package/dist/browser/cdp.js +339 -0
- package/dist/browser/daemon-client.d.ts +3 -1
- package/dist/browser/daemon-client.js +4 -0
- package/dist/browser/dom-helpers.d.ts +20 -0
- package/dist/browser/dom-helpers.js +109 -0
- package/dist/browser/index.d.ts +3 -0
- package/dist/browser/index.js +4 -0
- package/dist/browser/mcp.d.ts +1 -0
- package/dist/browser/mcp.js +10 -5
- package/dist/browser/page.d.ts +7 -0
- package/dist/browser/page.js +39 -123
- package/dist/browser/utils.d.ts +10 -0
- package/dist/browser/utils.js +27 -0
- package/dist/browser.test.js +49 -1
- package/dist/build-manifest.js +3 -1
- package/dist/build-manifest.test.js +34 -0
- package/dist/capabilityRouting.d.ts +2 -0
- package/dist/capabilityRouting.js +30 -0
- package/dist/capabilityRouting.test.d.ts +1 -0
- package/dist/capabilityRouting.test.js +42 -0
- package/dist/chaoxing.d.ts +58 -0
- package/dist/chaoxing.js +225 -0
- package/dist/chaoxing.test.d.ts +1 -0
- package/dist/chaoxing.test.js +45 -0
- package/dist/cli-manifest.json +885 -48
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +234 -0
- package/dist/clis/antigravity/serve.d.ts +14 -0
- package/dist/clis/antigravity/serve.js +263 -0
- package/dist/clis/bilibili/download.js +4 -14
- package/dist/clis/boss/chatlist.d.ts +1 -0
- package/dist/clis/boss/chatlist.js +50 -0
- package/dist/clis/boss/chatmsg.d.ts +1 -0
- package/dist/clis/boss/chatmsg.js +73 -0
- package/dist/clis/boss/resume.d.ts +1 -0
- package/dist/clis/boss/resume.js +249 -0
- package/dist/clis/boss/send.d.ts +1 -0
- package/dist/clis/boss/send.js +176 -0
- package/dist/clis/chaoxing/assignments.d.ts +1 -0
- package/dist/clis/chaoxing/assignments.js +74 -0
- package/dist/clis/chaoxing/exams.d.ts +1 -0
- package/dist/clis/chaoxing/exams.js +74 -0
- package/dist/clis/chatgpt/ask.js +15 -14
- package/dist/clis/chatgpt/ax.d.ts +1 -0
- package/dist/clis/chatgpt/ax.js +78 -0
- package/dist/clis/chatgpt/read.js +5 -6
- package/dist/clis/hf/top.d.ts +1 -0
- package/dist/clis/hf/top.js +119 -0
- package/dist/clis/jike/comment.d.ts +1 -0
- package/dist/clis/jike/comment.js +107 -0
- package/dist/clis/jike/create.d.ts +1 -0
- package/dist/clis/jike/create.js +106 -0
- package/dist/clis/jike/feed.d.ts +1 -0
- package/dist/clis/jike/feed.js +67 -0
- package/dist/clis/jike/like.d.ts +1 -0
- package/dist/clis/jike/like.js +61 -0
- package/dist/clis/jike/notifications.d.ts +1 -0
- package/dist/clis/jike/notifications.js +169 -0
- package/dist/clis/jike/post.yaml +58 -0
- package/dist/clis/jike/repost.d.ts +1 -0
- package/dist/clis/jike/repost.js +103 -0
- package/dist/clis/jike/search.d.ts +1 -0
- package/dist/clis/jike/search.js +67 -0
- package/dist/clis/jike/shared.d.ts +19 -0
- package/dist/clis/jike/shared.js +25 -0
- package/dist/clis/jike/topic.yaml +52 -0
- package/dist/clis/jike/user.yaml +51 -0
- package/dist/clis/smzdm/search.js +28 -39
- package/dist/clis/stackoverflow/bounties.yaml +29 -0
- package/dist/clis/stackoverflow/hot.yaml +28 -0
- package/dist/clis/stackoverflow/search.yaml +32 -0
- package/dist/clis/stackoverflow/unanswered.yaml +28 -0
- package/dist/clis/twitter/download.js +6 -16
- package/dist/clis/twitter/post.js +9 -2
- package/dist/clis/twitter/search.js +14 -33
- package/dist/clis/xiaohongshu/download.d.ts +1 -1
- package/dist/clis/xiaohongshu/download.js +4 -4
- package/dist/clis/zhihu/download.js +3 -3
- package/dist/doctor.d.ts +7 -0
- package/dist/doctor.js +16 -0
- package/dist/download/index.d.ts +12 -8
- package/dist/download/index.js +11 -3
- package/dist/download/index.test.d.ts +1 -0
- package/dist/download/index.test.js +14 -0
- package/dist/engine.js +25 -14
- package/dist/explore.d.ts +1 -0
- package/dist/explore.js +48 -103
- package/dist/generate.js +1 -0
- package/dist/interceptor.js +3 -2
- package/dist/main.js +4 -193
- package/dist/output.d.ts +2 -1
- package/dist/output.js +3 -1
- package/dist/pipeline/executor.test.js +1 -0
- package/dist/pipeline/steps/download.js +14 -18
- package/dist/registry.d.ts +4 -3
- package/dist/registry.js +5 -2
- package/dist/runtime.d.ts +4 -1
- package/dist/runtime.js +2 -2
- package/dist/scripts/framework.d.ts +4 -0
- package/dist/scripts/framework.js +21 -0
- package/dist/scripts/interact.d.ts +4 -0
- package/dist/scripts/interact.js +20 -0
- package/dist/scripts/store.d.ts +9 -0
- package/dist/scripts/store.js +44 -0
- package/dist/synthesize.js +1 -1
- package/dist/types.d.ts +12 -0
- package/dist/verify.d.ts +6 -1
- package/dist/verify.js +54 -2
- package/docs/.vitepress/config.mts +193 -0
- package/docs/adapters/browser/apple-podcasts.md +28 -0
- package/docs/adapters/browser/bbc.md +26 -0
- package/docs/adapters/browser/bilibili.md +38 -0
- package/docs/adapters/browser/boss.md +28 -0
- package/docs/adapters/browser/coupang.md +28 -0
- package/docs/adapters/browser/ctrip.md +27 -0
- package/docs/adapters/browser/github.md +26 -0
- package/docs/adapters/browser/hackernews.md +26 -0
- package/docs/adapters/browser/linkedin.md +27 -0
- package/docs/adapters/browser/reddit.md +41 -0
- package/docs/adapters/browser/reuters.md +27 -0
- package/docs/adapters/browser/smzdm.md +27 -0
- package/docs/adapters/browser/twitter.md +47 -0
- package/docs/adapters/browser/v2ex.md +32 -0
- package/docs/adapters/browser/weibo.md +27 -0
- package/docs/adapters/browser/xiaohongshu.md +32 -0
- package/docs/adapters/browser/xiaoyuzhou.md +28 -0
- package/docs/adapters/browser/xueqiu.md +32 -0
- package/docs/adapters/browser/yahoo-finance.md +26 -0
- package/docs/adapters/browser/youtube.md +29 -0
- package/docs/adapters/browser/zhihu.md +30 -0
- package/docs/adapters/desktop/antigravity.md +46 -0
- package/docs/adapters/desktop/chatgpt.md +43 -0
- package/docs/adapters/desktop/chatwise.md +38 -0
- package/docs/adapters/desktop/codex.md +32 -0
- package/docs/adapters/desktop/cursor.md +33 -0
- package/docs/adapters/desktop/discord.md +28 -0
- package/docs/adapters/desktop/feishu.md +20 -0
- package/docs/adapters/desktop/neteasemusic.md +31 -0
- package/docs/adapters/desktop/notion.md +29 -0
- package/docs/adapters/desktop/wechat.md +28 -0
- package/docs/adapters/index.md +49 -0
- package/docs/advanced/cdp.md +103 -0
- package/docs/advanced/download.md +63 -0
- package/docs/advanced/electron.md +125 -0
- package/docs/advanced/remote-chrome.md +72 -0
- package/docs/developer/ai-workflow.md +66 -0
- package/docs/developer/architecture.md +90 -0
- package/docs/developer/contributing.md +136 -0
- package/docs/developer/testing.md +237 -0
- package/docs/developer/ts-adapter.md +87 -0
- package/docs/developer/yaml-adapter.md +108 -0
- package/docs/guide/browser-bridge.md +38 -0
- package/docs/guide/getting-started.md +56 -0
- package/docs/guide/installation.md +37 -0
- package/docs/guide/troubleshooting.md +56 -0
- package/docs/index.md +35 -0
- package/docs/zh/adapters/index.md +5 -0
- package/docs/zh/advanced/cdp.md +3 -0
- package/docs/zh/developer/contributing.md +24 -0
- package/docs/zh/guide/browser-bridge.md +25 -0
- package/docs/zh/guide/getting-started.md +40 -0
- package/docs/zh/guide/installation.md +37 -0
- package/docs/zh/index.md +29 -0
- package/extension/dist/background.js +386 -438
- package/extension/manifest.json +2 -2
- package/extension/package-lock.json +1156 -0
- package/extension/src/background.test.ts +151 -0
- package/extension/src/background.ts +124 -53
- package/extension/src/protocol.ts +3 -1
- package/package.json +7 -3
- package/src/browser/cdp.ts +367 -0
- package/src/browser/daemon-client.ts +7 -1
- package/src/browser/dom-helpers.ts +116 -0
- package/src/browser/index.ts +4 -0
- package/src/browser/mcp.ts +14 -6
- package/src/browser/page.ts +47 -124
- package/src/browser/utils.ts +27 -0
- package/src/browser.test.ts +56 -0
- package/src/build-manifest.test.ts +36 -0
- package/src/build-manifest.ts +2 -1
- package/src/capabilityRouting.test.ts +47 -0
- package/src/capabilityRouting.ts +28 -0
- package/src/chaoxing.test.ts +53 -0
- package/src/chaoxing.ts +268 -0
- package/src/cli.ts +205 -0
- package/src/clis/antigravity/SKILL.md +5 -0
- package/src/clis/antigravity/serve.ts +329 -0
- package/src/clis/bilibili/download.ts +4 -15
- package/src/clis/boss/chatlist.ts +50 -0
- package/src/clis/boss/chatmsg.ts +70 -0
- package/src/clis/boss/resume.ts +262 -0
- package/src/clis/boss/send.ts +193 -0
- package/src/clis/chaoxing/README.md +36 -0
- package/src/clis/chaoxing/README.zh-CN.md +35 -0
- package/src/clis/chaoxing/assignments.ts +88 -0
- package/src/clis/chaoxing/exams.ts +88 -0
- package/src/clis/chatgpt/ask.ts +14 -15
- package/src/clis/chatgpt/ax.ts +81 -0
- package/src/clis/chatgpt/read.ts +5 -7
- package/src/clis/hf/top.ts +141 -0
- package/src/clis/jike/comment.ts +113 -0
- package/src/clis/jike/create.ts +113 -0
- package/src/clis/jike/feed.ts +74 -0
- package/src/clis/jike/like.ts +65 -0
- package/src/clis/jike/notifications.ts +185 -0
- package/src/clis/jike/post.yaml +58 -0
- package/src/clis/jike/repost.ts +114 -0
- package/src/clis/jike/search.ts +74 -0
- package/src/clis/jike/shared.ts +36 -0
- package/src/clis/jike/topic.yaml +52 -0
- package/src/clis/jike/user.yaml +51 -0
- package/src/clis/smzdm/search.ts +30 -39
- package/src/clis/stackoverflow/bounties.yaml +29 -0
- package/src/clis/stackoverflow/hot.yaml +28 -0
- package/src/clis/stackoverflow/search.yaml +32 -0
- package/src/clis/stackoverflow/unanswered.yaml +28 -0
- package/src/clis/twitter/download.ts +6 -17
- package/src/clis/twitter/post.ts +9 -2
- package/src/clis/twitter/search.ts +15 -33
- package/src/clis/xiaohongshu/download.ts +4 -4
- package/src/clis/zhihu/download.ts +3 -3
- package/src/doctor.ts +18 -2
- package/src/download/index.test.ts +16 -0
- package/src/download/index.ts +22 -4
- package/src/engine.ts +20 -13
- package/src/explore.ts +54 -103
- package/src/generate.ts +1 -0
- package/src/interceptor.ts +3 -2
- package/src/main.ts +4 -180
- package/src/output.ts +15 -13
- package/src/pipeline/executor.test.ts +1 -0
- package/src/pipeline/steps/download.ts +14 -17
- package/src/registry.ts +9 -5
- package/src/runtime.ts +3 -2
- package/src/scripts/framework.ts +20 -0
- package/src/scripts/interact.ts +22 -0
- package/src/scripts/store.ts +40 -0
- package/src/synthesize.ts +1 -1
- package/src/types.ts +9 -0
- package/src/verify.ts +64 -3
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* antigravity serve — Anthropic-compatible `/v1/messages` proxy server.
|
|
3
|
+
*
|
|
4
|
+
* Starts an HTTP server that accepts Anthropic Messages API requests,
|
|
5
|
+
* forwards them to a running Antigravity app via CDP, polls for the response,
|
|
6
|
+
* and returns it in Anthropic format.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* OPENCLI_CDP_ENDPOINT=http://127.0.0.1:9224 opencli antigravity serve --port 8082
|
|
10
|
+
* ANTHROPIC_BASE_URL=http://localhost:8082 claude
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
|
|
14
|
+
import { CDPBridge } from '../../browser/cdp.js';
|
|
15
|
+
import type { IPage } from '../../types.js';
|
|
16
|
+
|
|
17
|
+
// ─── Types ───────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
interface AnthropicRequest {
|
|
20
|
+
model?: string;
|
|
21
|
+
max_tokens?: number;
|
|
22
|
+
system?: string | Array<{ type: string; text: string }>;
|
|
23
|
+
messages: Array<{ role: string; content: string | Array<{ type: string; text?: string }> }>;
|
|
24
|
+
stream?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface AnthropicResponse {
|
|
28
|
+
id: string;
|
|
29
|
+
type: 'message';
|
|
30
|
+
role: 'assistant';
|
|
31
|
+
content: Array<{ type: 'text'; text: string }>;
|
|
32
|
+
model: string;
|
|
33
|
+
stop_reason: 'end_turn' | 'max_tokens' | 'stop_sequence';
|
|
34
|
+
stop_sequence: null;
|
|
35
|
+
usage: { input_tokens: number; output_tokens: number };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ─── Helpers ─────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
function generateMsgId(): string {
|
|
41
|
+
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
42
|
+
let id = 'msg_';
|
|
43
|
+
for (let i = 0; i < 24; i++) id += chars[Math.floor(Math.random() * chars.length)];
|
|
44
|
+
return id;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function estimateTokens(text: string): number {
|
|
48
|
+
// Rough approximation: ~4 chars per token for English, ~2 for CJK
|
|
49
|
+
return Math.max(1, Math.ceil(text.length / 3));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function extractTextContent(content: string | Array<{ type: string; text?: string }>): string {
|
|
53
|
+
if (typeof content === 'string') return content;
|
|
54
|
+
return content
|
|
55
|
+
.filter(b => b.type === 'text' && b.text)
|
|
56
|
+
.map(b => b.text!)
|
|
57
|
+
.join('\n');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function readBody(req: IncomingMessage): Promise<string> {
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const chunks: Buffer[] = [];
|
|
63
|
+
req.on('data', (c: Buffer) => chunks.push(c));
|
|
64
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
65
|
+
req.on('error', reject);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function jsonResponse(res: ServerResponse, status: number, data: unknown): void {
|
|
70
|
+
const body = JSON.stringify(data);
|
|
71
|
+
res.writeHead(status, {
|
|
72
|
+
'Content-Type': 'application/json',
|
|
73
|
+
'Access-Control-Allow-Origin': '*',
|
|
74
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
75
|
+
'Access-Control-Allow-Headers': 'Content-Type, x-api-key, anthropic-version, Authorization',
|
|
76
|
+
});
|
|
77
|
+
res.end(body);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function sleep(ms: number): Promise<void> {
|
|
81
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ─── Antigravity CDP Operations ──────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
async function getConversationText(page: IPage): Promise<string> {
|
|
87
|
+
const text = await page.evaluate(`
|
|
88
|
+
(() => {
|
|
89
|
+
const container = document.getElementById('conversation');
|
|
90
|
+
return container ? container.innerText : '';
|
|
91
|
+
})()
|
|
92
|
+
`);
|
|
93
|
+
return String(text ?? '');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function sendMessage(page: IPage, message: string): Promise<void> {
|
|
97
|
+
await page.evaluate(`
|
|
98
|
+
(async () => {
|
|
99
|
+
const container = document.getElementById('antigravity.agentSidePanelInputBox');
|
|
100
|
+
if (!container) throw new Error('Could not find antigravity.agentSidePanelInputBox');
|
|
101
|
+
const editor = container.querySelector('[data-lexical-editor="true"]');
|
|
102
|
+
if (!editor) throw new Error('Could not find Antigravity input box');
|
|
103
|
+
|
|
104
|
+
editor.focus();
|
|
105
|
+
document.execCommand('insertText', false, ${JSON.stringify(message)});
|
|
106
|
+
})()
|
|
107
|
+
`);
|
|
108
|
+
await sleep(500);
|
|
109
|
+
await page.pressKey('Enter');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function waitForReply(
|
|
113
|
+
page: IPage,
|
|
114
|
+
beforeText: string,
|
|
115
|
+
opts: { timeout?: number; pollInterval?: number; stableThreshold?: number } = {},
|
|
116
|
+
): Promise<string> {
|
|
117
|
+
const timeout = opts.timeout ?? 120_000; // 2 minutes max
|
|
118
|
+
const pollInterval = opts.pollInterval ?? 500; // 500ms polling
|
|
119
|
+
const stableThreshold = opts.stableThreshold ?? 6; // 6 × 500ms = 3s stable
|
|
120
|
+
|
|
121
|
+
const deadline = Date.now() + timeout;
|
|
122
|
+
let lastText = beforeText;
|
|
123
|
+
let stableCount = 0;
|
|
124
|
+
|
|
125
|
+
// Wait a bit for the model to start generating
|
|
126
|
+
await sleep(1000);
|
|
127
|
+
|
|
128
|
+
while (Date.now() < deadline) {
|
|
129
|
+
const current = await getConversationText(page);
|
|
130
|
+
|
|
131
|
+
if (current.length > beforeText.length) {
|
|
132
|
+
// New content appeared
|
|
133
|
+
if (current === lastText) {
|
|
134
|
+
stableCount++;
|
|
135
|
+
if (stableCount >= stableThreshold) {
|
|
136
|
+
// Text has been stable — reply is complete
|
|
137
|
+
return current.slice(beforeText.length).trim();
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
// Still generating
|
|
141
|
+
stableCount = 0;
|
|
142
|
+
lastText = current;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
await sleep(pollInterval);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Timeout — return whatever we have
|
|
150
|
+
const finalText = await getConversationText(page);
|
|
151
|
+
if (finalText.length > beforeText.length) {
|
|
152
|
+
return finalText.slice(beforeText.length).trim();
|
|
153
|
+
}
|
|
154
|
+
throw new Error('Timeout waiting for Antigravity reply');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ─── Request Handlers ────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
async function handleMessages(
|
|
160
|
+
body: AnthropicRequest,
|
|
161
|
+
page: IPage,
|
|
162
|
+
): Promise<AnthropicResponse> {
|
|
163
|
+
// Extract the last user message
|
|
164
|
+
const userMessages = body.messages.filter(m => m.role === 'user');
|
|
165
|
+
if (userMessages.length === 0) {
|
|
166
|
+
throw new Error('No user message found in request');
|
|
167
|
+
}
|
|
168
|
+
const lastUserMsg = userMessages[userMessages.length - 1];
|
|
169
|
+
const userText = extractTextContent(lastUserMsg.content);
|
|
170
|
+
|
|
171
|
+
if (!userText.trim()) {
|
|
172
|
+
throw new Error('Empty user message');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Get conversation state before sending
|
|
176
|
+
const beforeText = await getConversationText(page);
|
|
177
|
+
|
|
178
|
+
// Send the message
|
|
179
|
+
console.error(`[serve] Sending: "${userText.slice(0, 80)}${userText.length > 80 ? '...' : ''}"`);
|
|
180
|
+
await sendMessage(page, userText);
|
|
181
|
+
|
|
182
|
+
// Poll for reply
|
|
183
|
+
console.error('[serve] Waiting for reply...');
|
|
184
|
+
const replyText = await waitForReply(page, beforeText);
|
|
185
|
+
console.error(`[serve] Got reply: "${replyText.slice(0, 80)}${replyText.length > 80 ? '...' : ''}"`);
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
id: generateMsgId(),
|
|
189
|
+
type: 'message',
|
|
190
|
+
role: 'assistant',
|
|
191
|
+
content: [{ type: 'text', text: replyText }],
|
|
192
|
+
model: body.model ?? 'antigravity',
|
|
193
|
+
stop_reason: 'end_turn',
|
|
194
|
+
stop_sequence: null,
|
|
195
|
+
usage: {
|
|
196
|
+
input_tokens: estimateTokens(userText),
|
|
197
|
+
output_tokens: estimateTokens(replyText),
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ─── Server ──────────────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
export async function startServe(opts: { port?: number } = {}): Promise<void> {
|
|
205
|
+
const port = opts.port ?? 8082;
|
|
206
|
+
|
|
207
|
+
// Establish persistent CDP connection
|
|
208
|
+
console.error('[serve] Connecting to Antigravity via CDP...');
|
|
209
|
+
const cdp = new CDPBridge();
|
|
210
|
+
const page = await cdp.connect({ timeout: 15_000 });
|
|
211
|
+
console.error('[serve] CDP connected successfully.');
|
|
212
|
+
|
|
213
|
+
// Verify we can read conversation
|
|
214
|
+
const testText = await getConversationText(page);
|
|
215
|
+
console.error(`[serve] Conversation element found (${testText.length} chars).`);
|
|
216
|
+
|
|
217
|
+
let requestInFlight = false;
|
|
218
|
+
|
|
219
|
+
const server = createServer(async (req, res) => {
|
|
220
|
+
// CORS preflight
|
|
221
|
+
if (req.method === 'OPTIONS') {
|
|
222
|
+
res.writeHead(204, {
|
|
223
|
+
'Access-Control-Allow-Origin': '*',
|
|
224
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
225
|
+
'Access-Control-Allow-Headers': 'Content-Type, x-api-key, anthropic-version, Authorization',
|
|
226
|
+
});
|
|
227
|
+
res.end();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const url = req.url ?? '/';
|
|
232
|
+
const pathname = url.split('?')[0];
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
// GET /v1/models — return available models
|
|
236
|
+
if (req.method === 'GET' && pathname === '/v1/models') {
|
|
237
|
+
jsonResponse(res, 200, {
|
|
238
|
+
data: [
|
|
239
|
+
{
|
|
240
|
+
id: 'antigravity',
|
|
241
|
+
object: 'model',
|
|
242
|
+
created: Math.floor(Date.now() / 1000),
|
|
243
|
+
owned_by: 'antigravity',
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
});
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// POST /v1/messages — main endpoint
|
|
251
|
+
if (req.method === 'POST' && pathname === '/v1/messages') {
|
|
252
|
+
if (requestInFlight) {
|
|
253
|
+
jsonResponse(res, 429, {
|
|
254
|
+
type: 'error',
|
|
255
|
+
error: {
|
|
256
|
+
type: 'rate_limit_error',
|
|
257
|
+
message: 'Another request is currently being processed. Antigravity can only handle one request at a time.',
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
requestInFlight = true;
|
|
264
|
+
try {
|
|
265
|
+
const rawBody = await readBody(req);
|
|
266
|
+
const body = JSON.parse(rawBody) as AnthropicRequest;
|
|
267
|
+
|
|
268
|
+
if (body.stream) {
|
|
269
|
+
// We don't support streaming — return error
|
|
270
|
+
jsonResponse(res, 400, {
|
|
271
|
+
type: 'error',
|
|
272
|
+
error: {
|
|
273
|
+
type: 'invalid_request_error',
|
|
274
|
+
message: 'Streaming is not supported. Set "stream": false.',
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const response = await handleMessages(body, page);
|
|
281
|
+
jsonResponse(res, 200, response);
|
|
282
|
+
} finally {
|
|
283
|
+
requestInFlight = false;
|
|
284
|
+
}
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Health check
|
|
289
|
+
if (req.method === 'GET' && (pathname === '/' || pathname === '/health')) {
|
|
290
|
+
jsonResponse(res, 200, { ok: true, status: 'connected' });
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
jsonResponse(res, 404, {
|
|
295
|
+
type: 'error',
|
|
296
|
+
error: { type: 'not_found_error', message: `Not found: ${pathname}` },
|
|
297
|
+
});
|
|
298
|
+
} catch (err) {
|
|
299
|
+
console.error('[serve] Error:', err);
|
|
300
|
+
jsonResponse(res, 500, {
|
|
301
|
+
type: 'error',
|
|
302
|
+
error: {
|
|
303
|
+
type: 'api_error',
|
|
304
|
+
message: err instanceof Error ? err.message : 'Internal server error',
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
server.listen(port, '127.0.0.1', () => {
|
|
311
|
+
console.error(`\n[serve] ✅ Antigravity API proxy running at http://127.0.0.1:${port}`);
|
|
312
|
+
console.error(`[serve] Compatible with Anthropic /v1/messages API`);
|
|
313
|
+
console.error(`\n[serve] Usage with Claude Code:`);
|
|
314
|
+
console.error(` ANTHROPIC_BASE_URL=http://localhost:${port} claude\n`);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// Graceful shutdown
|
|
318
|
+
const shutdown = () => {
|
|
319
|
+
console.error('\n[serve] Shutting down...');
|
|
320
|
+
cdp.close().catch(() => {});
|
|
321
|
+
server.close();
|
|
322
|
+
process.exit(0);
|
|
323
|
+
};
|
|
324
|
+
process.on('SIGTERM', shutdown);
|
|
325
|
+
process.on('SIGINT', shutdown);
|
|
326
|
+
|
|
327
|
+
// Keep alive
|
|
328
|
+
await new Promise(() => {});
|
|
329
|
+
}
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
sanitizeFilename,
|
|
18
18
|
getTempDir,
|
|
19
19
|
exportCookiesToNetscape,
|
|
20
|
+
formatCookieHeader,
|
|
20
21
|
} from '../../download/index.js';
|
|
21
22
|
import { DownloadProgressTracker, formatBytes } from '../../download/progress.js';
|
|
22
23
|
|
|
@@ -63,30 +64,18 @@ cli({
|
|
|
63
64
|
const title = sanitizeFilename(data?.title || 'video');
|
|
64
65
|
|
|
65
66
|
// Extract cookies for authenticated downloads
|
|
66
|
-
const
|
|
67
|
+
const cookies = await page.getCookies({ domain: 'bilibili.com' });
|
|
68
|
+
const cookieString = formatCookieHeader(cookies);
|
|
67
69
|
|
|
68
70
|
// Create output directory
|
|
69
71
|
fs.mkdirSync(output, { recursive: true });
|
|
70
72
|
|
|
71
73
|
// Export cookies to Netscape format for yt-dlp
|
|
72
74
|
let cookiesFile: string | undefined;
|
|
73
|
-
if (
|
|
75
|
+
if (cookies.length > 0) {
|
|
74
76
|
const tempDir = getTempDir();
|
|
75
77
|
fs.mkdirSync(tempDir, { recursive: true });
|
|
76
78
|
cookiesFile = path.join(tempDir, `bilibili_cookies_${Date.now()}.txt`);
|
|
77
|
-
|
|
78
|
-
const cookies = cookieString.split(';').map((c) => {
|
|
79
|
-
const [name, ...rest] = c.trim().split('=');
|
|
80
|
-
return {
|
|
81
|
-
name: name || '',
|
|
82
|
-
value: rest.join('=') || '',
|
|
83
|
-
domain: '.bilibili.com',
|
|
84
|
-
path: '/',
|
|
85
|
-
secure: true,
|
|
86
|
-
httpOnly: false,
|
|
87
|
-
};
|
|
88
|
-
}).filter((c) => c.name);
|
|
89
|
-
|
|
90
79
|
exportCookiesToNetscape(cookies, cookiesFile);
|
|
91
80
|
}
|
|
92
81
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import type { IPage } from '../../types.js';
|
|
3
|
+
|
|
4
|
+
cli({
|
|
5
|
+
site: 'boss',
|
|
6
|
+
name: 'chatlist',
|
|
7
|
+
description: 'BOSS直聘查看聊天列表(招聘端)',
|
|
8
|
+
domain: 'www.zhipin.com',
|
|
9
|
+
strategy: Strategy.COOKIE,
|
|
10
|
+
browser: true,
|
|
11
|
+
args: [
|
|
12
|
+
{ name: 'page', type: 'int', default: 1, help: 'Page number' },
|
|
13
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Number of results' },
|
|
14
|
+
{ name: 'job_id', default: '0', help: 'Filter by job ID (0=all)' },
|
|
15
|
+
],
|
|
16
|
+
columns: ['name', 'job', 'last_msg', 'last_time', 'uid', 'security_id'],
|
|
17
|
+
func: async (page: IPage | null, kwargs) => {
|
|
18
|
+
if (!page) throw new Error('Browser page required');
|
|
19
|
+
await page.goto('https://www.zhipin.com/web/chat/index');
|
|
20
|
+
await page.wait({ time: 2 });
|
|
21
|
+
const jobId = kwargs.job_id || '0';
|
|
22
|
+
const pageNum = kwargs.page || 1;
|
|
23
|
+
const limit = kwargs.limit || 20;
|
|
24
|
+
const targetUrl = `https://www.zhipin.com/wapi/zprelation/friend/getBossFriendListV2.json?page=${pageNum}&status=0&jobId=${jobId}`;
|
|
25
|
+
const data: any = await page.evaluate(`
|
|
26
|
+
async () => {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
const xhr = new XMLHttpRequest();
|
|
29
|
+
xhr.open('GET', '${targetUrl}', true);
|
|
30
|
+
xhr.withCredentials = true;
|
|
31
|
+
xhr.timeout = 15000;
|
|
32
|
+
xhr.setRequestHeader('Accept', 'application/json');
|
|
33
|
+
xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { reject(new Error('JSON parse failed')); } };
|
|
34
|
+
xhr.onerror = () => reject(new Error('Network Error'));
|
|
35
|
+
xhr.send();
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
`);
|
|
39
|
+
if (data.code !== 0) throw new Error(`API error: ${data.message} (code=${data.code})`);
|
|
40
|
+
const friends = (data.zpData?.friendList || []).slice(0, limit);
|
|
41
|
+
return friends.map((f: any) => ({
|
|
42
|
+
name: f.name || '',
|
|
43
|
+
job: f.jobName || '',
|
|
44
|
+
last_msg: f.lastMessageInfo?.text || '',
|
|
45
|
+
last_time: f.lastTime || '',
|
|
46
|
+
uid: f.encryptUid || '',
|
|
47
|
+
security_id: f.securityId || '',
|
|
48
|
+
}));
|
|
49
|
+
},
|
|
50
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import type { IPage } from '../../types.js';
|
|
3
|
+
|
|
4
|
+
cli({
|
|
5
|
+
site: 'boss',
|
|
6
|
+
name: 'chatmsg',
|
|
7
|
+
description: 'BOSS直聘查看与候选人的聊天消息',
|
|
8
|
+
domain: 'www.zhipin.com',
|
|
9
|
+
strategy: Strategy.COOKIE,
|
|
10
|
+
browser: true,
|
|
11
|
+
args: [
|
|
12
|
+
{ name: 'uid', required: true, help: 'Encrypted UID (from chatlist)' },
|
|
13
|
+
{ name: 'page', type: 'int', default: 1, help: 'Page number' },
|
|
14
|
+
],
|
|
15
|
+
columns: ['from', 'type', 'text', 'time'],
|
|
16
|
+
func: async (page: IPage | null, kwargs) => {
|
|
17
|
+
if (!page) throw new Error('Browser page required');
|
|
18
|
+
await page.goto('https://www.zhipin.com/web/chat/index');
|
|
19
|
+
await page.wait({ time: 2 });
|
|
20
|
+
const uid = kwargs.uid;
|
|
21
|
+
const friendData: any = await page.evaluate(`
|
|
22
|
+
async () => {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const xhr = new XMLHttpRequest();
|
|
25
|
+
xhr.open('GET', 'https://www.zhipin.com/wapi/zprelation/friend/getBossFriendListV2.json?page=1&status=0&jobId=0', true);
|
|
26
|
+
xhr.withCredentials = true;
|
|
27
|
+
xhr.timeout = 15000;
|
|
28
|
+
xhr.setRequestHeader('Accept', 'application/json');
|
|
29
|
+
xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { reject(e); } };
|
|
30
|
+
xhr.onerror = () => reject(new Error('Network Error'));
|
|
31
|
+
xhr.send();
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
`);
|
|
35
|
+
if (friendData.code !== 0) throw new Error('获取好友列表失败');
|
|
36
|
+
const friend = (friendData.zpData?.friendList || []).find((f: any) => f.encryptUid === uid);
|
|
37
|
+
if (!friend) throw new Error('未找到该候选人');
|
|
38
|
+
const gid = friend.uid;
|
|
39
|
+
const securityId = encodeURIComponent(friend.securityId);
|
|
40
|
+
const msgUrl = `https://www.zhipin.com/wapi/zpchat/boss/historyMsg?gid=${gid}&securityId=${securityId}&page=${kwargs.page}&c=20&src=0`;
|
|
41
|
+
const msgData: any = await page.evaluate(`
|
|
42
|
+
async () => {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const xhr = new XMLHttpRequest();
|
|
45
|
+
xhr.open('GET', '${msgUrl}', true);
|
|
46
|
+
xhr.withCredentials = true;
|
|
47
|
+
xhr.timeout = 15000;
|
|
48
|
+
xhr.setRequestHeader('Accept', 'application/json');
|
|
49
|
+
xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { resolve({raw: xhr.responseText.substring(0,500)}); } };
|
|
50
|
+
xhr.onerror = () => reject(new Error('Network Error'));
|
|
51
|
+
xhr.send();
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
`);
|
|
55
|
+
if (msgData.raw) throw new Error('Non-JSON: ' + msgData.raw);
|
|
56
|
+
if (msgData.code !== 0) throw new Error('API error: ' + (msgData.message || msgData.code));
|
|
57
|
+
const TYPE_MAP: Record<number, string> = {1: '文本', 2: '图片', 3: '招呼', 4: '简历', 5: '系统', 6: '名片', 7: '语音', 8: '视频', 9: '表情'};
|
|
58
|
+
const messages = msgData.zpData?.messages || msgData.zpData?.historyMsgList || [];
|
|
59
|
+
return messages.map((m: any) => {
|
|
60
|
+
const fromObj = m.from || {};
|
|
61
|
+
const isSelf = typeof fromObj === 'object' ? fromObj.uid !== friend.uid : false;
|
|
62
|
+
return {
|
|
63
|
+
from: isSelf ? '我' : (typeof fromObj === 'object' ? fromObj.name : friend.name),
|
|
64
|
+
type: TYPE_MAP[m.type] || '其他(' + m.type + ')',
|
|
65
|
+
text: m.text || m.body?.text || '',
|
|
66
|
+
time: m.time ? new Date(m.time).toLocaleString('zh-CN') : '',
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
});
|