mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-16 08:14:45 +00:00
feat(browser): 增加 artifact 持久化与导入导出能力
新增 recording、replay、orchestration 的本地 JSON artifact 导出、导入、列出与删除能力。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
1 parent
d0e6087126
commit
2f167d8445
2 files changed
+450
No files matched your search
@@ -72,6 +72,10 @@ const TOOL_CANCEL_REPLAY = 'browser_cancel_replay';
|
||||
const TOOL_START_ORCHESTRATION = 'browser_start_orchestration';
|
||||
const TOOL_GET_ORCHESTRATION = 'browser_get_orchestration';
|
||||
const TOOL_CANCEL_ORCHESTRATION = 'browser_cancel_orchestration';
|
||||
const TOOL_EXPORT_ARTIFACT = 'browser_export_artifact';
|
||||
const TOOL_IMPORT_ARTIFACT = 'browser_import_artifact';
|
||||
const TOOL_LIST_ARTIFACTS = 'browser_list_artifacts';
|
||||
const TOOL_DELETE_ARTIFACT = 'browser_delete_artifact';
|
||||
const TOOL_TAKE_SCREENSHOT = 'browser_take_screenshot';
|
||||
const TOOL_WAIT_FOR = 'browser_wait_for';
|
||||
const TOOL_EVAL = 'browser_eval';
|
||||
@@ -113,6 +117,10 @@ const TOOL_NAMES = [
|
||||
TOOL_START_ORCHESTRATION,
|
||||
TOOL_GET_ORCHESTRATION,
|
||||
TOOL_CANCEL_ORCHESTRATION,
|
||||
TOOL_EXPORT_ARTIFACT,
|
||||
TOOL_IMPORT_ARTIFACT,
|
||||
TOOL_LIST_ARTIFACTS,
|
||||
TOOL_DELETE_ARTIFACT,
|
||||
TOOL_TAKE_SCREENSHOT,
|
||||
TOOL_WAIT_FOR,
|
||||
TOOL_EVAL,
|
||||
@@ -841,6 +849,52 @@ function getTools() {
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: TOOL_EXPORT_ARTIFACT,
|
||||
description: 'Export the current recording, replay, or orchestration artifact to a local JSON file.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
kind: { type: 'string' },
|
||||
name: { type: 'string' },
|
||||
},
|
||||
required: ['kind', 'name'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: TOOL_IMPORT_ARTIFACT,
|
||||
description: 'Import a recording, replay, or orchestration artifact from a local JSON file into session-local state.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: { type: 'string' },
|
||||
},
|
||||
required: ['path'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: TOOL_LIST_ARTIFACTS,
|
||||
description: 'List locally saved Browser MCP artifacts.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: TOOL_DELETE_ARTIFACT,
|
||||
description: 'Delete a locally saved Browser MCP artifact by name.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
},
|
||||
required: ['name'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: TOOL_TAKE_SCREENSHOT,
|
||||
description:
|
||||
@@ -2392,6 +2446,52 @@ function formatRecordingSummary(session) {
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function getArtifactDir() {
|
||||
const scoped = Buffer.from(process.cwd()).toString('base64url');
|
||||
const baseDir = path.join(os.tmpdir(), 'ccs-browser-artifacts', scoped);
|
||||
if (!fs.existsSync(baseDir)) {
|
||||
fs.mkdirSync(baseDir, { recursive: true });
|
||||
}
|
||||
return baseDir;
|
||||
}
|
||||
|
||||
function getArtifactPath(name) {
|
||||
return path.join(getArtifactDir(), `${name}.json`);
|
||||
}
|
||||
|
||||
function buildArtifact(kind, name, payload) {
|
||||
return {
|
||||
kind,
|
||||
version: 1,
|
||||
name,
|
||||
createdAt: new Date().toISOString(),
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
function readArtifactFile(filePath) {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error('artifact file not found');
|
||||
}
|
||||
const raw = fs.readFileSync(filePath, 'utf8');
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(raw);
|
||||
} catch {
|
||||
throw new Error('invalid artifact payload');
|
||||
}
|
||||
if (!parsed || typeof parsed !== 'object') {
|
||||
throw new Error('invalid artifact payload');
|
||||
}
|
||||
if (parsed.version !== 1) {
|
||||
throw new Error('unsupported artifact version');
|
||||
}
|
||||
if (typeof parsed.kind !== 'string' || typeof parsed.name !== 'string' || !('payload' in parsed)) {
|
||||
throw new Error('invalid artifact payload');
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function createOrchestrationId() {
|
||||
const orchestrationId = `orc_${String(nextOrchestrationCounter).padStart(4, '0')}`;
|
||||
nextOrchestrationCounter += 1;
|
||||
@@ -5265,6 +5365,91 @@ async function handleGetOrchestration() {
|
||||
return formatOrchestrationSummary(session);
|
||||
}
|
||||
|
||||
function getArtifactPayloadForKind(kind) {
|
||||
if (kind === 'recording') {
|
||||
return requireLatestRecording();
|
||||
}
|
||||
if (kind === 'replay') {
|
||||
return requireLatestReplay();
|
||||
}
|
||||
if (kind === 'orchestration') {
|
||||
return requireLatestOrchestration();
|
||||
}
|
||||
throw new Error(`unsupported artifact kind: ${kind}`);
|
||||
}
|
||||
|
||||
function resolveArtifactPath(reference) {
|
||||
const value = requireNonEmptyString(reference, 'path');
|
||||
if (value.startsWith('artifact:')) {
|
||||
return getArtifactPath(value.slice('artifact:'.length));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function deleteArtifactByName(name) {
|
||||
const filePath = getArtifactPath(name);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error('artifact not found');
|
||||
}
|
||||
fs.unlinkSync(filePath);
|
||||
return filePath;
|
||||
}
|
||||
|
||||
async function handleExportArtifact(toolArgs) {
|
||||
const kind = requireNonEmptyString(toolArgs.kind, 'kind');
|
||||
const name = requireNonEmptyString(toolArgs.name, 'name');
|
||||
const filePath = getArtifactPath(name);
|
||||
if (fs.existsSync(filePath)) {
|
||||
throw new Error('artifact already exists');
|
||||
}
|
||||
const payload = getArtifactPayloadForKind(kind);
|
||||
const artifact = buildArtifact(kind, name, payload);
|
||||
fs.writeFileSync(filePath, JSON.stringify(artifact, null, 2), 'utf8');
|
||||
return `name: ${name}\nkind: ${kind}\npath: ${filePath}\nstatus: exported`;
|
||||
}
|
||||
|
||||
async function handleListArtifacts() {
|
||||
const dir = getArtifactDir();
|
||||
const entries = fs
|
||||
.readdirSync(dir)
|
||||
.filter((entry) => entry.endsWith('.json'))
|
||||
.map((entry) => readArtifactFile(path.join(dir, entry)));
|
||||
if (entries.length === 0) {
|
||||
return 'artifacts: []';
|
||||
}
|
||||
return entries
|
||||
.map((entry) => [
|
||||
`name: ${entry.name}`,
|
||||
`kind: ${entry.kind}`,
|
||||
`createdAt: ${entry.createdAt}`,
|
||||
`path: ${getArtifactPath(entry.name)}`,
|
||||
].join('\n'))
|
||||
.join('\n---\n');
|
||||
}
|
||||
|
||||
async function handleImportArtifact(toolArgs) {
|
||||
const filePath = resolveArtifactPath(toolArgs.path);
|
||||
const artifact = readArtifactFile(filePath);
|
||||
|
||||
if (artifact.kind === 'recording') {
|
||||
latestRecordingSession = artifact.payload;
|
||||
} else if (artifact.kind === 'replay') {
|
||||
latestReplaySession = artifact.payload;
|
||||
} else if (artifact.kind === 'orchestration') {
|
||||
latestOrchestrationSession = artifact.payload;
|
||||
} else {
|
||||
throw new Error('artifact kind mismatch');
|
||||
}
|
||||
|
||||
return `name: ${artifact.name}\nkind: ${artifact.kind}\nstatus: imported`;
|
||||
}
|
||||
|
||||
async function handleDeleteArtifact(toolArgs) {
|
||||
const name = requireNonEmptyString(toolArgs.name, 'name');
|
||||
deleteArtifactByName(name);
|
||||
return `name: ${name}\nstatus: deleted`;
|
||||
}
|
||||
|
||||
async function handleCancelOrchestration() {
|
||||
if (!activeOrchestrationSession) {
|
||||
throw new Error('no active orchestration');
|
||||
@@ -5544,6 +5729,38 @@ async function handleToolCall(message) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (toolName === TOOL_EXPORT_ARTIFACT) {
|
||||
const text = await handleExportArtifact(toolArgs);
|
||||
writeResponse(id, {
|
||||
content: [{ type: 'text', text }],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (toolName === TOOL_IMPORT_ARTIFACT) {
|
||||
const text = await handleImportArtifact(toolArgs);
|
||||
writeResponse(id, {
|
||||
content: [{ type: 'text', text }],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (toolName === TOOL_LIST_ARTIFACTS) {
|
||||
const text = await handleListArtifacts(toolArgs);
|
||||
writeResponse(id, {
|
||||
content: [{ type: 'text', text }],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (toolName === TOOL_DELETE_ARTIFACT) {
|
||||
const text = await handleDeleteArtifact(toolArgs);
|
||||
writeResponse(id, {
|
||||
content: [{ type: 'text', text }],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (toolName === TOOL_TAKE_SCREENSHOT) {
|
||||
const text = await handleScreenshot(toolArgs);
|
||||
writeResponse(id, {
|
||||
|
||||
Reference in new issue
Block a user