Commit a38906e4 authored by edy's avatar edy

feat(desktop): restrict automation research sources

parent ef2f3d16
Pipeline #18477 failed
...@@ -167,6 +167,23 @@ const SUPPORTED_ATTACHMENT_EXTENSIONS = new Set([ ...@@ -167,6 +167,23 @@ const SUPPORTED_ATTACHMENT_EXTENSIONS = new Set([
...DOCUMENT_ATTACHMENT_EXTENSIONS ...DOCUMENT_ATTACHMENT_EXTENSIONS
]); ]);
const BUILTIN_HOME_PROJECT_ID = "home-chat"; const BUILTIN_HOME_PROJECT_ID = "home-chat";
const AUTOMATION_DOMESTIC_SOURCE_CONSTRAINT_MARKER = "自动化联网资料源限制:";
const AUTOMATION_DOMESTIC_SOURCE_CONSTRAINT = [
AUTOMATION_DOMESTIC_SOURCE_CONSTRAINT_MARKER,
"- 只允许搜索、打开、引用中国境内网站资料;按域名和来源机构判断,不按服务器物理 IP 判断。",
"- 禁止使用 GoogleWikipediaYouTubeTwitter/XReddit、海外媒体、海外 SaaS 文档等非国内资料源。",
"- 优先使用 .cn、政府、央媒、行业协会、国内平台资料;可使用 baidu.comqq.com163.comsina.com.cnsohu.compeople.com.cnxinhuanet.comcctv.com 等国内来源。",
"- 如果搜索工具支持 domain allowlist,只使用国内域名 allowlist;不支持时,用 site:.cn 或国内域名 site: 范围搜索并过滤结果。",
"- 最终回复不得引用非国内来源;找不到足够国内资料时,明确回复“未找到可用国内资料”。"
].join("\n");
function withAutomationDomesticSourceConstraint(prompt: string): string {
const trimmed = prompt.trim();
if (trimmed.endsWith(AUTOMATION_DOMESTIC_SOURCE_CONSTRAINT)) {
return trimmed;
}
return [trimmed, AUTOMATION_DOMESTIC_SOURCE_CONSTRAINT].filter(Boolean).join("\n\n");
}
function inferAttachmentMimeType(localPath: string, name?: string): string { function inferAttachmentMimeType(localPath: string, name?: string): string {
const extension = path.extname(name || localPath).toLowerCase(); const extension = path.extname(name || localPath).toLowerCase();
...@@ -2057,6 +2074,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -2057,6 +2074,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
}; };
const executeAutomationPrompt = async (sessionId: string, projectId: string, prompt: string) => { const executeAutomationPrompt = async (sessionId: string, projectId: string, prompt: string) => {
const automationPrompt = withAutomationDomesticSourceConstraint(prompt);
const project = await projectStore.getProjectSummary(projectId); const project = await projectStore.getProjectSummary(projectId);
const projectRoot = await projectStore.getProjectRoot(project.id); const projectRoot = await projectStore.getProjectRoot(project.id);
const projectConfig = await projectStore.getProjectPackageConfig(project.id); const projectConfig = await projectStore.getProjectPackageConfig(project.id);
...@@ -2065,7 +2083,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -2065,7 +2083,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
sessionId, sessionId,
projectId: project.id, projectId: project.id,
projectRoot, projectRoot,
userPrompt: prompt, userPrompt: automationPrompt,
context: snapshot, context: snapshot,
selectedSkillId: null, selectedSkillId: null,
attachments: [], attachments: [],
...@@ -2088,7 +2106,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -2088,7 +2106,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
sessionId, sessionId,
projectId: project.id, projectId: project.id,
projectRoot, projectRoot,
userPrompt: prompt, userPrompt: automationPrompt,
context: snapshot, context: snapshot,
selectedSkillId: resolvedSkillRoute.skillId, selectedSkillId: resolvedSkillRoute.skillId,
attachments: [], attachments: [],
...@@ -2109,7 +2127,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -2109,7 +2127,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
sessionId, sessionId,
projectRoot, projectRoot,
prompt: decision.preparedPrompt, prompt: decision.preparedPrompt,
userPrompt: prompt, userPrompt: automationPrompt,
attachments: [], attachments: [],
extraEnv: { extraEnv: {
...projectModelEnv, ...projectModelEnv,
...@@ -2120,7 +2138,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -2120,7 +2138,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
const fallbackExecutionPolicy = await resolveExecutionPolicy(project.id, undefined, "chat-fallback"); const fallbackExecutionPolicy = await resolveExecutionPolicy(project.id, undefined, "chat-fallback");
const fallbackResult = await runGatewayChatRequestWithRecovery(chatGatewayRecoveryCoordinator, { const fallbackResult = await runGatewayChatRequestWithRecovery(chatGatewayRecoveryCoordinator, {
reason: "automation-task", reason: "automation-task",
execute: () => gatewayClient.sendPrompt(sessionId, result.handoff.content) execute: () => gatewayClient.sendPrompt(sessionId, withAutomationDomesticSourceConstraint(result.handoff.content))
}); });
runtimeCloudSupervisor.noteMessageSent( runtimeCloudSupervisor.noteMessageSent(
fallbackResult.sessionId, fallbackResult.sessionId,
...@@ -2141,7 +2159,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -2141,7 +2159,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
const result = await runGatewayChatRequestWithRecovery(chatGatewayRecoveryCoordinator, { const result = await runGatewayChatRequestWithRecovery(chatGatewayRecoveryCoordinator, {
reason: "automation-task", reason: "automation-task",
execute: () => gatewayClient.sendPrompt(sessionId, gatewayPrompt ?? prompt) execute: () => gatewayClient.sendPrompt(sessionId, gatewayPrompt ?? automationPrompt)
}); });
runtimeCloudSupervisor.noteMessageSent(result.sessionId, result.reply.content, executionPolicy.modelId, executionSkillId); runtimeCloudSupervisor.noteMessageSent(result.sessionId, result.reply.content, executionPolicy.modelId, executionSkillId);
return { ...result, executionPolicy, artifacts: [] }; return { ...result, executionPolicy, artifacts: [] };
......
...@@ -19,6 +19,18 @@ function getExecuteAutomationPromptBody(): string { ...@@ -19,6 +19,18 @@ function getExecuteAutomationPromptBody(): string {
return bodyMatch.groups.body return bodyMatch.groups.body
} }
function getSendPromptBody(): string {
const bodyMatch = ipcSource.match(/const sendPrompt = async \(sessionId: string, prompt: string, skillId\?: string, attachments\?: ChatAttachment\[\]\) => \{(?<body>[\s\S]*?)\n \};\n\n const executeAutomationPrompt = async/)
assert.ok(bodyMatch?.groups?.body)
return bodyMatch.groups.body
}
function getStreamPromptBody(): string {
const bodyMatch = ipcSource.match(/const streamPrompt = async \(sessionId: string, prompt: string, skillId\?: string, attachments\?: ChatAttachment\[\], sender\?: WebContents\) => \{(?<body>[\s\S]*?)\n \};\n const cancelStream = async/)
assert.ok(bodyMatch?.groups?.body)
return bodyMatch.groups.body
}
test("shared automation task channels and desktop API are declared", () => { test("shared automation task channels and desktop API are declared", () => {
assert.match(sharedTypesSource, /automationTasksList:\s*"automation-tasks:list"/) assert.match(sharedTypesSource, /automationTasksList:\s*"automation-tasks:list"/)
assert.match(sharedTypesSource, /automationTasksCreate:\s*"automation-tasks:create"/) assert.match(sharedTypesSource, /automationTasksCreate:\s*"automation-tasks:create"/)
...@@ -88,3 +100,30 @@ test("automation prompt execution refreshes project context after completion wit ...@@ -88,3 +100,30 @@ test("automation prompt execution refreshes project context after completion wit
assert.match(refreshCallMatch.groups.args, /projectStore/) assert.match(refreshCallMatch.groups.args, /projectStore/)
assert.doesNotMatch(refreshCallMatch.groups.args, /sessionId/) assert.doesNotMatch(refreshCallMatch.groups.args, /sessionId/)
}) })
test("automation prompts restrict online research to domestic China sources", () => {
const automationPromptBody = getExecuteAutomationPromptBody()
assert.match(ipcSource, /AUTOMATION_DOMESTIC_SOURCE_CONSTRAINT/)
assert.match(ipcSource, /中国境内网站资料/)
assert.match(ipcSource, /未找到可用国内资料/)
assert.match(ipcSource, /Google、Wikipedia、YouTube、Twitter\/X、Reddit/)
assert.match(ipcSource, /baidu\.com、qq\.com、163\.com、sina\.com\.cn、sohu\.com、people\.com\.cn、xinhuanet\.com、cctv\.com/)
assert.match(automationPromptBody, /const automationPrompt = withAutomationDomesticSourceConstraint\(prompt\)/)
assert.match(automationPromptBody, /userPrompt: automationPrompt/)
assert.match(automationPromptBody, /projectSkillRouter\.resolve\(project\.id, prompt\)/)
assert.match(automationPromptBody, /userPrompt: automationPrompt/)
assert.match(automationPromptBody, /userPrompt: automationPrompt,\n\s+attachments: \[\]/)
assert.match(automationPromptBody, /gatewayClient\.sendPrompt\(sessionId, withAutomationDomesticSourceConstraint\(result\.handoff\.content\)\)/)
assert.match(automationPromptBody, /gatewayClient\.sendPrompt\(sessionId, gatewayPrompt \?\? automationPrompt\)/)
})
test("automation domestic source guard cannot be bypassed by marker text alone", () => {
assert.doesNotMatch(ipcSource, /trimmed\.includes\(AUTOMATION_DOMESTIC_SOURCE_CONSTRAINT_MARKER\)/)
assert.match(ipcSource, /trimmed\.endsWith\(AUTOMATION_DOMESTIC_SOURCE_CONSTRAINT\)/)
})
test("ordinary chat prompts do not receive automation domestic source constraints", () => {
assert.doesNotMatch(getSendPromptBody(), /withAutomationDomesticSourceConstraint/)
assert.doesNotMatch(getStreamPromptBody(), /withAutomationDomesticSourceConstraint/)
})
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment