@reconcrap/boss-recommend-mcp 1.3.39 → 2.0.0
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/README.md +53 -33
- package/package.json +61 -9
- package/skills/boss-recommend-pipeline/SKILL.md +4 -0
- package/src/chat-mcp.js +1333 -0
- package/src/chat-runtime-config.js +559 -0
- package/src/cli.js +1095 -196
- package/src/core/browser/index.js +378 -0
- package/src/core/capture/index.js +298 -0
- package/src/core/cv-acquisition/index.js +219 -0
- package/src/core/greet-quota/index.js +54 -0
- package/src/core/infinite-list/index.js +459 -0
- package/src/core/reporting/legacy-csv.js +332 -0
- package/src/core/run/index.js +286 -0
- package/src/core/screening/index.js +1166 -0
- package/src/core/self-heal/index.js +848 -0
- package/src/domains/chat/cards.js +129 -0
- package/src/domains/chat/constants.js +183 -0
- package/src/domains/chat/detail.js +1369 -0
- package/src/domains/chat/index.js +7 -0
- package/src/domains/chat/jobs.js +334 -0
- package/src/domains/chat/page-guard.js +88 -0
- package/src/domains/chat/roots.js +56 -0
- package/src/domains/chat/run-service.js +1101 -0
- package/src/domains/recommend/actions.js +457 -0
- package/src/domains/recommend/cards.js +228 -0
- package/src/domains/recommend/constants.js +141 -0
- package/src/domains/recommend/detail.js +341 -0
- package/src/domains/recommend/filters.js +581 -0
- package/src/domains/recommend/index.js +10 -0
- package/src/domains/recommend/jobs.js +232 -0
- package/src/domains/recommend/refresh.js +204 -0
- package/src/domains/recommend/roots.js +78 -0
- package/src/domains/recommend/run-service.js +903 -0
- package/src/domains/recommend/scopes.js +245 -0
- package/src/domains/recruit/actions.js +277 -0
- package/src/domains/recruit/cards.js +67 -0
- package/src/domains/recruit/constants.js +130 -0
- package/src/domains/recruit/detail.js +414 -0
- package/src/domains/recruit/index.js +9 -0
- package/src/domains/recruit/instruction-parser.js +451 -0
- package/src/domains/recruit/refresh.js +40 -0
- package/src/domains/recruit/roots.js +68 -0
- package/src/domains/recruit/run-service.js +580 -0
- package/src/domains/recruit/search.js +1149 -0
- package/src/index.js +578 -419
- package/src/recommend-mcp.js +1257 -0
- package/src/recruit-mcp.js +1035 -0
- package/src/adapters.js +0 -3079
- package/src/boss-chat.js +0 -1037
- package/src/pipeline.js +0 -2249
- package/src/recommend-healing-config.js +0 -131
- package/src/recommend-healing-rules.json +0 -261
- package/src/self-heal.js +0 -2237
- package/src/test-adapters-runtime.js +0 -628
- package/src/test-boss-chat.js +0 -3196
- package/src/test-index-async.js +0 -498
- package/src/test-parser.js +0 -742
- package/src/test-pipeline.js +0 -2703
- package/src/test-run-state.js +0 -152
- package/src/test-self-heal.js +0 -224
- package/vendor/boss-chat-cli/README.md +0 -134
- package/vendor/boss-chat-cli/package.json +0 -53
- package/vendor/boss-chat-cli/src/app.js +0 -1501
- package/vendor/boss-chat-cli/src/browser/chat-page.js +0 -3562
- package/vendor/boss-chat-cli/src/cli.js +0 -1713
- package/vendor/boss-chat-cli/src/mcp/server.js +0 -149
- package/vendor/boss-chat-cli/src/mcp/tool-runtime.js +0 -193
- package/vendor/boss-chat-cli/src/runtime/async-run-state.js +0 -260
- package/vendor/boss-chat-cli/src/runtime/interaction.js +0 -102
- package/vendor/boss-chat-cli/src/runtime/run-control.js +0 -102
- package/vendor/boss-chat-cli/src/services/chrome-client.js +0 -107
- package/vendor/boss-chat-cli/src/services/llm.js +0 -1292
- package/vendor/boss-chat-cli/src/services/llm.test.js +0 -326
- package/vendor/boss-chat-cli/src/services/profile-store.js +0 -173
- package/vendor/boss-chat-cli/src/services/report-store.js +0 -317
- package/vendor/boss-chat-cli/src/services/resume-capture.js +0 -469
- package/vendor/boss-chat-cli/src/services/resume-network.js +0 -727
- package/vendor/boss-chat-cli/src/services/state-store.js +0 -90
- package/vendor/boss-chat-cli/src/utils/customer-key.js +0 -82
- package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +0 -7072
- package/vendor/boss-recommend-screen-cli/scripts/capture-full-resume-canvas.cjs +0 -817
- package/vendor/boss-recommend-screen-cli/scripts/stitch_resume_chunks.py +0 -141
- package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +0 -2423
- package/vendor/boss-recommend-search-cli/src/cli.js +0 -1698
- package/vendor/boss-recommend-search-cli/src/test-job-selection.js +0 -211
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import process from 'node:process';
|
|
3
|
-
|
|
4
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
5
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
6
|
-
import * as z from 'zod/v4';
|
|
7
|
-
|
|
8
|
-
import { runCliJsonCommand } from './tool-runtime.js';
|
|
9
|
-
|
|
10
|
-
const SERVER_VERSION = process.env.BOSS_CHAT_MCP_VERSION || '1.1.0';
|
|
11
|
-
|
|
12
|
-
function toToolResult(payload, isError = false) {
|
|
13
|
-
return {
|
|
14
|
-
content: [
|
|
15
|
-
{
|
|
16
|
-
type: 'text',
|
|
17
|
-
text: JSON.stringify(payload, null, 2),
|
|
18
|
-
},
|
|
19
|
-
],
|
|
20
|
-
structuredContent: payload,
|
|
21
|
-
isError,
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async function executeCliCommand(command, input) {
|
|
26
|
-
const result = await runCliJsonCommand(command, input);
|
|
27
|
-
const payload = {
|
|
28
|
-
...result.payload,
|
|
29
|
-
_meta: {
|
|
30
|
-
command: result.command,
|
|
31
|
-
args: result.args,
|
|
32
|
-
exitCode: result.exitCode,
|
|
33
|
-
stderr: String(result.stderr || '').trim() || undefined,
|
|
34
|
-
},
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
if (!payload._meta.stderr) {
|
|
38
|
-
delete payload._meta.stderr;
|
|
39
|
-
}
|
|
40
|
-
return toToolResult(payload, !result.ok);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function registerTools(server) {
|
|
44
|
-
server.registerTool(
|
|
45
|
-
'health_check',
|
|
46
|
-
{
|
|
47
|
-
description: '检查 MCP 服务与 CLI 适配层是否可用。',
|
|
48
|
-
inputSchema: {},
|
|
49
|
-
},
|
|
50
|
-
async () =>
|
|
51
|
-
toToolResult({
|
|
52
|
-
status: 'OK',
|
|
53
|
-
server: 'boss-chat-mcp',
|
|
54
|
-
version: SERVER_VERSION,
|
|
55
|
-
supportedAgents: ['openclaw', 'codex', 'trae-cn'],
|
|
56
|
-
}),
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
server.registerTool(
|
|
60
|
-
'start_run',
|
|
61
|
-
{
|
|
62
|
-
description: '异步启动一次 Boss chat 任务并返回 run_id。',
|
|
63
|
-
inputSchema: {
|
|
64
|
-
profile: z.string().optional().describe('Profile 名称,默认 default'),
|
|
65
|
-
dryRun: z.boolean().optional().describe('true 时不发出索要简历动作'),
|
|
66
|
-
noState: z.boolean().optional().describe('true 时不记录已处理状态'),
|
|
67
|
-
job: z.string().describe('岗位,支持岗位名/编号/value'),
|
|
68
|
-
startFrom: z
|
|
69
|
-
.enum(['unread', 'all'])
|
|
70
|
-
.optional()
|
|
71
|
-
.describe('从未读或全部聊天列表开始'),
|
|
72
|
-
criteria: z.string().describe('筛选标准'),
|
|
73
|
-
targetCount: z.number().int().positive().optional().describe('本次处理上限'),
|
|
74
|
-
baseUrl: z.string().optional().describe('覆盖 LLM baseUrl'),
|
|
75
|
-
apiKey: z.string().optional().describe('覆盖 LLM apiKey'),
|
|
76
|
-
model: z.string().optional().describe('覆盖 LLM 模型'),
|
|
77
|
-
thinkingLevel: z.string().optional().describe('覆盖 LLM thinking/reasoning 级别:off/low/medium/high/current'),
|
|
78
|
-
port: z.number().int().positive().optional().describe('Chrome 调试端口'),
|
|
79
|
-
safePacing: z.boolean().optional().describe('是否启用安全节奏控制'),
|
|
80
|
-
batchRestEnabled: z.boolean().optional().describe('是否启用批次休息'),
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
async (input) => executeCliCommand('start-run', input),
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
server.registerTool(
|
|
87
|
-
'get_run',
|
|
88
|
-
{
|
|
89
|
-
description: '查询 run_id 对应任务的当前状态。',
|
|
90
|
-
inputSchema: {
|
|
91
|
-
runId: z.string().min(1).describe('start_run 返回的 run_id'),
|
|
92
|
-
profile: z.string().optional().describe('可选,默认 default'),
|
|
93
|
-
},
|
|
94
|
-
},
|
|
95
|
-
async (input) => executeCliCommand('get-run', input),
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
server.registerTool(
|
|
99
|
-
'pause_run',
|
|
100
|
-
{
|
|
101
|
-
description: '暂停运行中的任务。',
|
|
102
|
-
inputSchema: {
|
|
103
|
-
runId: z.string().min(1).describe('start_run 返回的 run_id'),
|
|
104
|
-
profile: z.string().optional().describe('可选,默认 default'),
|
|
105
|
-
},
|
|
106
|
-
},
|
|
107
|
-
async (input) => executeCliCommand('pause-run', input),
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
server.registerTool(
|
|
111
|
-
'resume_run',
|
|
112
|
-
{
|
|
113
|
-
description: '继续已暂停任务。',
|
|
114
|
-
inputSchema: {
|
|
115
|
-
runId: z.string().min(1).describe('start_run 返回的 run_id'),
|
|
116
|
-
profile: z.string().optional().describe('可选,默认 default'),
|
|
117
|
-
},
|
|
118
|
-
},
|
|
119
|
-
async (input) => executeCliCommand('resume-run', input),
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
server.registerTool(
|
|
123
|
-
'cancel_run',
|
|
124
|
-
{
|
|
125
|
-
description: '取消运行中的任务(在安全点停止)。',
|
|
126
|
-
inputSchema: {
|
|
127
|
-
runId: z.string().min(1).describe('start_run 返回的 run_id'),
|
|
128
|
-
profile: z.string().optional().describe('可选,默认 default'),
|
|
129
|
-
},
|
|
130
|
-
},
|
|
131
|
-
async (input) => executeCliCommand('cancel-run', input),
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
async function main() {
|
|
136
|
-
const server = new McpServer({
|
|
137
|
-
name: 'boss-chat-mcp',
|
|
138
|
-
version: SERVER_VERSION,
|
|
139
|
-
});
|
|
140
|
-
registerTools(server);
|
|
141
|
-
|
|
142
|
-
const transport = new StdioServerTransport();
|
|
143
|
-
await server.connect(transport);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
main().catch((error) => {
|
|
147
|
-
console.error('[boss-chat-mcp] server failed:', error?.stack || error?.message || String(error));
|
|
148
|
-
process.exit(1);
|
|
149
|
-
});
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
import process from 'node:process';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
|
|
5
|
-
const CLI_PATH = fileURLToPath(new URL('../cli.js', import.meta.url));
|
|
6
|
-
|
|
7
|
-
function pushValueArg(args, name, value) {
|
|
8
|
-
if (value === undefined || value === null || value === '') {
|
|
9
|
-
return;
|
|
10
|
-
}
|
|
11
|
-
args.push(`--${name}`, String(value));
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function pushBooleanArg(args, name, value) {
|
|
15
|
-
if (value === true) {
|
|
16
|
-
args.push(`--${name}`);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function parseJsonFromStdout(stdout) {
|
|
21
|
-
const trimmed = String(stdout || '').trim();
|
|
22
|
-
if (!trimmed) return null;
|
|
23
|
-
|
|
24
|
-
try {
|
|
25
|
-
return JSON.parse(trimmed);
|
|
26
|
-
} catch {}
|
|
27
|
-
|
|
28
|
-
const lines = trimmed
|
|
29
|
-
.split(/\r?\n/g)
|
|
30
|
-
.map((line) => line.trim())
|
|
31
|
-
.filter(Boolean);
|
|
32
|
-
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
33
|
-
try {
|
|
34
|
-
return JSON.parse(lines[index]);
|
|
35
|
-
} catch {}
|
|
36
|
-
}
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function normalizeStartFrom(input) {
|
|
41
|
-
const value = String(input || '').trim().toLowerCase();
|
|
42
|
-
if (value === 'all') return 'all';
|
|
43
|
-
return 'unread';
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function normalizePositiveInt(input) {
|
|
47
|
-
if (input === undefined || input === null || input === '') return null;
|
|
48
|
-
const value = Number.parseInt(String(input), 10);
|
|
49
|
-
if (!Number.isFinite(value) || value <= 0) return null;
|
|
50
|
-
return value;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function buildCliArgs(command, input = {}) {
|
|
54
|
-
const args = [String(command), '--json'];
|
|
55
|
-
pushValueArg(args, 'profile', input.profile || 'default');
|
|
56
|
-
pushBooleanArg(args, 'dry-run', input.dryRun);
|
|
57
|
-
pushBooleanArg(args, 'no-state', input.noState);
|
|
58
|
-
|
|
59
|
-
switch (command) {
|
|
60
|
-
case 'start-run':
|
|
61
|
-
case 'run': {
|
|
62
|
-
pushValueArg(args, 'job', input.job);
|
|
63
|
-
pushValueArg(args, 'start-from', normalizeStartFrom(input.startFrom));
|
|
64
|
-
pushValueArg(args, 'criteria', input.criteria);
|
|
65
|
-
|
|
66
|
-
const targetCount = normalizePositiveInt(input.targetCount);
|
|
67
|
-
if (targetCount) {
|
|
68
|
-
pushValueArg(args, 'targetCount', targetCount);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
pushValueArg(args, 'baseurl', input.baseUrl);
|
|
72
|
-
pushValueArg(args, 'apikey', input.apiKey);
|
|
73
|
-
pushValueArg(args, 'model', input.model);
|
|
74
|
-
pushValueArg(args, 'thinking-level', input.thinkingLevel || input.llmThinkingLevel || input.reasoningEffort);
|
|
75
|
-
|
|
76
|
-
const port = normalizePositiveInt(input.port);
|
|
77
|
-
if (port) {
|
|
78
|
-
pushValueArg(args, 'port', port);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (typeof input.safePacing === 'boolean') {
|
|
82
|
-
pushValueArg(args, 'safe-pacing', String(input.safePacing));
|
|
83
|
-
}
|
|
84
|
-
if (typeof input.batchRestEnabled === 'boolean') {
|
|
85
|
-
pushValueArg(args, 'batch-rest', String(input.batchRestEnabled));
|
|
86
|
-
}
|
|
87
|
-
break;
|
|
88
|
-
}
|
|
89
|
-
case 'get-run':
|
|
90
|
-
case 'pause-run':
|
|
91
|
-
case 'resume-run':
|
|
92
|
-
case 'cancel-run':
|
|
93
|
-
pushValueArg(args, 'run-id', input.runId);
|
|
94
|
-
break;
|
|
95
|
-
default:
|
|
96
|
-
break;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return args;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export async function runCliJsonCommand(command, input = {}) {
|
|
103
|
-
const cliArgs = buildCliArgs(command, input);
|
|
104
|
-
const cwd = String(input.cwd || process.cwd());
|
|
105
|
-
|
|
106
|
-
return new Promise((resolve) => {
|
|
107
|
-
const child = spawn(process.execPath, [CLI_PATH, ...cliArgs], {
|
|
108
|
-
cwd,
|
|
109
|
-
env: process.env,
|
|
110
|
-
windowsHide: true,
|
|
111
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
let stdout = '';
|
|
115
|
-
let stderr = '';
|
|
116
|
-
child.stdout.on('data', (chunk) => {
|
|
117
|
-
stdout += String(chunk);
|
|
118
|
-
});
|
|
119
|
-
child.stderr.on('data', (chunk) => {
|
|
120
|
-
stderr += String(chunk);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
child.on('error', (error) => {
|
|
124
|
-
resolve({
|
|
125
|
-
ok: false,
|
|
126
|
-
exitCode: -1,
|
|
127
|
-
command,
|
|
128
|
-
args: cliArgs,
|
|
129
|
-
stdout,
|
|
130
|
-
stderr,
|
|
131
|
-
payload: {
|
|
132
|
-
status: 'FAILED',
|
|
133
|
-
error: {
|
|
134
|
-
code: 'CLI_SPAWN_FAILED',
|
|
135
|
-
message: error?.message || '无法启动 boss-chat CLI',
|
|
136
|
-
retryable: true,
|
|
137
|
-
},
|
|
138
|
-
},
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
child.on('close', (code) => {
|
|
143
|
-
const exitCode = Number.isInteger(code) ? code : 1;
|
|
144
|
-
const parsed = parseJsonFromStdout(stdout);
|
|
145
|
-
|
|
146
|
-
if (parsed && typeof parsed === 'object') {
|
|
147
|
-
resolve({
|
|
148
|
-
ok: exitCode === 0 && parsed.status !== 'FAILED',
|
|
149
|
-
exitCode,
|
|
150
|
-
command,
|
|
151
|
-
args: cliArgs,
|
|
152
|
-
stdout,
|
|
153
|
-
stderr,
|
|
154
|
-
payload: parsed,
|
|
155
|
-
});
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
if (exitCode === 0) {
|
|
160
|
-
resolve({
|
|
161
|
-
ok: true,
|
|
162
|
-
exitCode,
|
|
163
|
-
command,
|
|
164
|
-
args: cliArgs,
|
|
165
|
-
stdout,
|
|
166
|
-
stderr,
|
|
167
|
-
payload: {
|
|
168
|
-
status: 'OK',
|
|
169
|
-
message: String(stdout || '').trim() || `${command} 执行成功`,
|
|
170
|
-
},
|
|
171
|
-
});
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
resolve({
|
|
176
|
-
ok: false,
|
|
177
|
-
exitCode,
|
|
178
|
-
command,
|
|
179
|
-
args: cliArgs,
|
|
180
|
-
stdout,
|
|
181
|
-
stderr,
|
|
182
|
-
payload: {
|
|
183
|
-
status: 'FAILED',
|
|
184
|
-
error: {
|
|
185
|
-
code: 'CLI_EXECUTION_FAILED',
|
|
186
|
-
message: String(stderr || stdout || '').trim() || `${command} 执行失败`,
|
|
187
|
-
retryable: true,
|
|
188
|
-
},
|
|
189
|
-
},
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
});
|
|
193
|
-
}
|
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
import crypto from 'node:crypto';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
|
|
5
|
-
export const RUN_MODE_ASYNC = 'async';
|
|
6
|
-
|
|
7
|
-
export const RUN_STATE_QUEUED = 'queued';
|
|
8
|
-
export const RUN_STATE_RUNNING = 'running';
|
|
9
|
-
export const RUN_STATE_PAUSED = 'paused';
|
|
10
|
-
export const RUN_STATE_COMPLETED = 'completed';
|
|
11
|
-
export const RUN_STATE_FAILED = 'failed';
|
|
12
|
-
export const RUN_STATE_CANCELED = 'canceled';
|
|
13
|
-
|
|
14
|
-
const TERMINAL_RUN_STATES = new Set([
|
|
15
|
-
RUN_STATE_COMPLETED,
|
|
16
|
-
RUN_STATE_FAILED,
|
|
17
|
-
RUN_STATE_CANCELED,
|
|
18
|
-
]);
|
|
19
|
-
|
|
20
|
-
function nowIso() {
|
|
21
|
-
return new Date().toISOString();
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function normalizeRunId(runId) {
|
|
25
|
-
const normalized = String(runId || '').trim();
|
|
26
|
-
if (!normalized || normalized.includes('/') || normalized.includes('\\')) {
|
|
27
|
-
throw new Error('Invalid run_id');
|
|
28
|
-
}
|
|
29
|
-
return normalized;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function ensureRunsDir(baseDir) {
|
|
33
|
-
const runsDir = path.join(baseDir, 'runs');
|
|
34
|
-
fs.mkdirSync(runsDir, { recursive: true });
|
|
35
|
-
return runsDir;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function createRunId() {
|
|
39
|
-
if (typeof crypto.randomUUID === 'function') {
|
|
40
|
-
return crypto.randomUUID();
|
|
41
|
-
}
|
|
42
|
-
return crypto.randomBytes(16).toString('hex');
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function getRunStatePath(baseDir, runId) {
|
|
46
|
-
const normalizedRunId = normalizeRunId(runId);
|
|
47
|
-
return path.join(ensureRunsDir(baseDir), `${normalizedRunId}.json`);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function getRunEventsPath(baseDir, runId) {
|
|
51
|
-
const normalizedRunId = normalizeRunId(runId);
|
|
52
|
-
return path.join(ensureRunsDir(baseDir), `${normalizedRunId}.events.jsonl`);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function writeJsonAtomic(filePath, payload) {
|
|
56
|
-
const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
57
|
-
fs.writeFileSync(tmpPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
58
|
-
fs.renameSync(tmpPath, filePath);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function clonePlain(value, fallback = null) {
|
|
62
|
-
try {
|
|
63
|
-
return JSON.parse(JSON.stringify(value));
|
|
64
|
-
} catch {
|
|
65
|
-
return fallback;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function normalizeProgress(progress = {}) {
|
|
70
|
-
return {
|
|
71
|
-
inspected:
|
|
72
|
-
Number.isInteger(progress.inspected) && progress.inspected >= 0
|
|
73
|
-
? progress.inspected
|
|
74
|
-
: 0,
|
|
75
|
-
passed:
|
|
76
|
-
Number.isInteger(progress.passed) && progress.passed >= 0 ? progress.passed : 0,
|
|
77
|
-
requested:
|
|
78
|
-
Number.isInteger(progress.requested) && progress.requested >= 0
|
|
79
|
-
? progress.requested
|
|
80
|
-
: 0,
|
|
81
|
-
skipped:
|
|
82
|
-
Number.isInteger(progress.skipped) && progress.skipped >= 0 ? progress.skipped : 0,
|
|
83
|
-
errors:
|
|
84
|
-
Number.isInteger(progress.errors) && progress.errors >= 0 ? progress.errors : 0,
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function normalizeControl(control = {}) {
|
|
89
|
-
return {
|
|
90
|
-
pauseRequested: control?.pauseRequested === true,
|
|
91
|
-
cancelRequested: control?.cancelRequested === true,
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function normalizeState(state) {
|
|
96
|
-
const normalized = String(state || '').trim().toLowerCase();
|
|
97
|
-
if (
|
|
98
|
-
[
|
|
99
|
-
RUN_STATE_QUEUED,
|
|
100
|
-
RUN_STATE_RUNNING,
|
|
101
|
-
RUN_STATE_PAUSED,
|
|
102
|
-
RUN_STATE_COMPLETED,
|
|
103
|
-
RUN_STATE_FAILED,
|
|
104
|
-
RUN_STATE_CANCELED,
|
|
105
|
-
].includes(normalized)
|
|
106
|
-
) {
|
|
107
|
-
return normalized;
|
|
108
|
-
}
|
|
109
|
-
return RUN_STATE_QUEUED;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export function createRunStateSnapshot({
|
|
113
|
-
runId,
|
|
114
|
-
pid = null,
|
|
115
|
-
request = null,
|
|
116
|
-
state = RUN_STATE_QUEUED,
|
|
117
|
-
stage = 'preflight',
|
|
118
|
-
lastMessage = '任务已创建,等待执行。',
|
|
119
|
-
} = {}) {
|
|
120
|
-
const createdAt = nowIso();
|
|
121
|
-
return {
|
|
122
|
-
runId: normalizeRunId(runId),
|
|
123
|
-
mode: RUN_MODE_ASYNC,
|
|
124
|
-
state: normalizeState(state),
|
|
125
|
-
stage: String(stage || 'preflight'),
|
|
126
|
-
createdAt,
|
|
127
|
-
updatedAt: createdAt,
|
|
128
|
-
heartbeatAt: createdAt,
|
|
129
|
-
pid: Number.isInteger(pid) && pid > 0 ? pid : null,
|
|
130
|
-
lastMessage: String(lastMessage || ''),
|
|
131
|
-
progress: normalizeProgress(),
|
|
132
|
-
control: normalizeControl(),
|
|
133
|
-
request: clonePlain(request, null),
|
|
134
|
-
error: null,
|
|
135
|
-
result: null,
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export function writeRunState(baseDir, snapshot) {
|
|
140
|
-
const normalizedSnapshot = {
|
|
141
|
-
...snapshot,
|
|
142
|
-
runId: normalizeRunId(snapshot?.runId),
|
|
143
|
-
mode: RUN_MODE_ASYNC,
|
|
144
|
-
state: normalizeState(snapshot?.state),
|
|
145
|
-
stage: String(snapshot?.stage || 'preflight'),
|
|
146
|
-
createdAt: String(snapshot?.createdAt || nowIso()),
|
|
147
|
-
updatedAt: String(snapshot?.updatedAt || nowIso()),
|
|
148
|
-
heartbeatAt: String(snapshot?.heartbeatAt || nowIso()),
|
|
149
|
-
pid:
|
|
150
|
-
Number.isInteger(snapshot?.pid) && snapshot.pid > 0
|
|
151
|
-
? snapshot.pid
|
|
152
|
-
: null,
|
|
153
|
-
lastMessage: String(snapshot?.lastMessage || ''),
|
|
154
|
-
progress: normalizeProgress(snapshot?.progress || {}),
|
|
155
|
-
control: normalizeControl(snapshot?.control || {}),
|
|
156
|
-
request: clonePlain(snapshot?.request, null),
|
|
157
|
-
error: Object.prototype.hasOwnProperty.call(snapshot || {}, 'error')
|
|
158
|
-
? snapshot.error
|
|
159
|
-
: null,
|
|
160
|
-
result: Object.prototype.hasOwnProperty.call(snapshot || {}, 'result')
|
|
161
|
-
? snapshot.result
|
|
162
|
-
: null,
|
|
163
|
-
};
|
|
164
|
-
writeJsonAtomic(getRunStatePath(baseDir, normalizedSnapshot.runId), normalizedSnapshot);
|
|
165
|
-
return normalizedSnapshot;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export function readRunState(baseDir, runId) {
|
|
169
|
-
try {
|
|
170
|
-
const raw = fs.readFileSync(getRunStatePath(baseDir, runId), 'utf8');
|
|
171
|
-
const parsed = JSON.parse(raw);
|
|
172
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
173
|
-
return null;
|
|
174
|
-
}
|
|
175
|
-
return {
|
|
176
|
-
...parsed,
|
|
177
|
-
runId: normalizeRunId(parsed.runId),
|
|
178
|
-
state: normalizeState(parsed.state),
|
|
179
|
-
progress: normalizeProgress(parsed.progress || {}),
|
|
180
|
-
control: normalizeControl(parsed.control || {}),
|
|
181
|
-
pid: Number.isInteger(parsed.pid) && parsed.pid > 0 ? parsed.pid : null,
|
|
182
|
-
mode: RUN_MODE_ASYNC,
|
|
183
|
-
stage: String(parsed.stage || 'preflight'),
|
|
184
|
-
lastMessage: String(parsed.lastMessage || ''),
|
|
185
|
-
createdAt: String(parsed.createdAt || ''),
|
|
186
|
-
updatedAt: String(parsed.updatedAt || ''),
|
|
187
|
-
heartbeatAt: String(parsed.heartbeatAt || ''),
|
|
188
|
-
};
|
|
189
|
-
} catch (error) {
|
|
190
|
-
if (error && error.code === 'ENOENT') {
|
|
191
|
-
return null;
|
|
192
|
-
}
|
|
193
|
-
throw error;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
export function updateRunState(baseDir, runId, updater) {
|
|
198
|
-
const current = readRunState(baseDir, runId);
|
|
199
|
-
if (!current) return null;
|
|
200
|
-
|
|
201
|
-
const patch =
|
|
202
|
-
typeof updater === 'function' ? updater(clonePlain(current, current)) : updater;
|
|
203
|
-
if (!patch || typeof patch !== 'object') {
|
|
204
|
-
return current;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const now = nowIso();
|
|
208
|
-
const next = {
|
|
209
|
-
...current,
|
|
210
|
-
...patch,
|
|
211
|
-
runId: current.runId,
|
|
212
|
-
mode: RUN_MODE_ASYNC,
|
|
213
|
-
state: normalizeState(patch.state ?? current.state),
|
|
214
|
-
stage: String(patch.stage ?? current.stage ?? 'preflight'),
|
|
215
|
-
progress: normalizeProgress({
|
|
216
|
-
...current.progress,
|
|
217
|
-
...(patch.progress || {}),
|
|
218
|
-
}),
|
|
219
|
-
control: normalizeControl({
|
|
220
|
-
...current.control,
|
|
221
|
-
...(patch.control || {}),
|
|
222
|
-
}),
|
|
223
|
-
lastMessage: Object.prototype.hasOwnProperty.call(patch, 'lastMessage')
|
|
224
|
-
? String(patch.lastMessage || '')
|
|
225
|
-
: current.lastMessage,
|
|
226
|
-
pid: Object.prototype.hasOwnProperty.call(patch, 'pid')
|
|
227
|
-
? Number.isInteger(patch.pid) && patch.pid > 0
|
|
228
|
-
? patch.pid
|
|
229
|
-
: null
|
|
230
|
-
: current.pid,
|
|
231
|
-
updatedAt: now,
|
|
232
|
-
heartbeatAt: Object.prototype.hasOwnProperty.call(patch, 'heartbeatAt')
|
|
233
|
-
? String(patch.heartbeatAt || now)
|
|
234
|
-
: current.heartbeatAt || now,
|
|
235
|
-
error: Object.prototype.hasOwnProperty.call(patch, 'error')
|
|
236
|
-
? patch.error
|
|
237
|
-
: current.error,
|
|
238
|
-
result: Object.prototype.hasOwnProperty.call(patch, 'result')
|
|
239
|
-
? patch.result
|
|
240
|
-
: current.result,
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
writeRunState(baseDir, next);
|
|
244
|
-
return next;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
export function appendRunEvent(baseDir, runId, event = {}) {
|
|
248
|
-
const filePath = getRunEventsPath(baseDir, runId);
|
|
249
|
-
const payload = {
|
|
250
|
-
timestamp: nowIso(),
|
|
251
|
-
runId: normalizeRunId(runId),
|
|
252
|
-
...clonePlain(event, {}),
|
|
253
|
-
};
|
|
254
|
-
fs.appendFileSync(filePath, `${JSON.stringify(payload)}\n`, 'utf8');
|
|
255
|
-
return payload;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
export function isTerminalRunState(state) {
|
|
259
|
-
return TERMINAL_RUN_STATES.has(normalizeState(state));
|
|
260
|
-
}
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
function sleep(ms) {
|
|
2
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
function gaussianRandom() {
|
|
6
|
-
let u = 0;
|
|
7
|
-
let v = 0;
|
|
8
|
-
while (u === 0) u = Math.random();
|
|
9
|
-
while (v === 0) v = Math.random();
|
|
10
|
-
return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function humanDelay(baseMs, varianceMs = 0) {
|
|
14
|
-
const sampled = baseMs + gaussianRandom() * (varianceMs / 3 || 1);
|
|
15
|
-
return Math.max(0, Math.round(sampled));
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export class InteractionController {
|
|
19
|
-
constructor(chromeClient, options = {}) {
|
|
20
|
-
this.chromeClient = chromeClient;
|
|
21
|
-
this.safePacing = options.safePacing !== false;
|
|
22
|
-
this.batchRestEnabled = options.batchRestEnabled !== false;
|
|
23
|
-
this.runControl = options.runControl || null;
|
|
24
|
-
this.nextRestAt = this.batchRestEnabled ? this.randomRestThreshold() : Number.POSITIVE_INFINITY;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
randomRestThreshold() {
|
|
28
|
-
return 15 + Math.floor(Math.random() * 11);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
async sleepRange(baseMs, varianceMs) {
|
|
32
|
-
const delay = this.safePacing ? humanDelay(baseMs, varianceMs) : baseMs;
|
|
33
|
-
await this.wait(delay);
|
|
34
|
-
return delay;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async wait(ms) {
|
|
38
|
-
if (this.runControl) {
|
|
39
|
-
await this.runControl.delay(ms);
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
await sleep(ms);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async maybeRest(processedCount, logger = console) {
|
|
46
|
-
if (!this.batchRestEnabled || processedCount < this.nextRestAt) {
|
|
47
|
-
return 0;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const restMs = 4000 + Math.floor(Math.random() * 4000);
|
|
51
|
-
logger.log(`短暂休息 ${restMs}ms,保持处理节奏稳定...`);
|
|
52
|
-
await this.wait(restMs);
|
|
53
|
-
this.nextRestAt = processedCount + this.randomRestThreshold();
|
|
54
|
-
return restMs;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async moveMouseNear(targetX, targetY) {
|
|
58
|
-
const steps = this.safePacing ? 4 + Math.floor(Math.random() * 3) : 2;
|
|
59
|
-
const startX = Math.round(targetX - 40 + Math.random() * 30);
|
|
60
|
-
const startY = Math.round(targetY - 20 + Math.random() * 20);
|
|
61
|
-
|
|
62
|
-
for (let index = 1; index <= steps; index += 1) {
|
|
63
|
-
const progress = index / steps;
|
|
64
|
-
const x = Math.round(startX + (targetX - startX) * progress + (Math.random() - 0.5) * 4);
|
|
65
|
-
const y = Math.round(startY + (targetY - startY) * progress + (Math.random() - 0.5) * 4);
|
|
66
|
-
await this.chromeClient.Input.dispatchMouseEvent({
|
|
67
|
-
type: 'mouseMoved',
|
|
68
|
-
x,
|
|
69
|
-
y,
|
|
70
|
-
});
|
|
71
|
-
await this.wait(10 + Math.floor(Math.random() * 25));
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async clickRect(rect) {
|
|
76
|
-
const centerX = rect.left + rect.width / 2;
|
|
77
|
-
const centerY = rect.top + rect.height / 2;
|
|
78
|
-
const offsetX = (Math.random() - 0.5) * Math.min(18, rect.width * 0.25);
|
|
79
|
-
const offsetY = (Math.random() - 0.5) * Math.min(12, rect.height * 0.25);
|
|
80
|
-
const targetX = Math.round(centerX + offsetX);
|
|
81
|
-
const targetY = Math.round(centerY + offsetY);
|
|
82
|
-
|
|
83
|
-
await this.moveMouseNear(targetX, targetY);
|
|
84
|
-
await this.sleepRange(180, 80);
|
|
85
|
-
|
|
86
|
-
await this.chromeClient.Input.dispatchMouseEvent({
|
|
87
|
-
type: 'mousePressed',
|
|
88
|
-
x: targetX,
|
|
89
|
-
y: targetY,
|
|
90
|
-
button: 'left',
|
|
91
|
-
clickCount: 1,
|
|
92
|
-
});
|
|
93
|
-
await this.wait(25 + Math.floor(Math.random() * 45));
|
|
94
|
-
await this.chromeClient.Input.dispatchMouseEvent({
|
|
95
|
-
type: 'mouseReleased',
|
|
96
|
-
x: targetX,
|
|
97
|
-
y: targetY,
|
|
98
|
-
button: 'left',
|
|
99
|
-
clickCount: 1,
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
}
|