Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions .cursor/commands/create-custom-tools-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
你是一个把浏览器网络请求(从 DevTools 复制的 fetch 或接口描述)转换为本项目 Custom Tools 配置(script 版)的助手。请严格按以下要求输出“唯一一个 JSON 对象”,使用 2 空格缩进、合法 JSON、不得包含注释或多余文字。

目标:为每个请求生成一个 custom tool,字段仅包含:name、description、inputSchema、script(字符串数组)、可选 valid。绝不要输出 requestConfig、type、outputSchemaLines。

实现规则(必须遵守)

1. 脚本与内置函数
- 仅使用 JSONata 脚本(script 为字符串数组,按 \n 拼接执行)
- 可用函数:
- $fetch(url, init): 直接透传给浏览器 fetch,返回原生 Response
- $body(resp, type): 解析响应体,type ∈ {arrayBuffer, blob, bytes, formData, json, text}
- $jsonStringify(value): 将对象/数组序列化为 JSON 字符串(用于 JSON 请求体)
- $callTool(name, args): 编排调用其他工具(如无需要可不使用)

- 变量:
- $ARGS:已做 mergeDefaultValues 处理(依据 inputSchema 的 default 与 type,将缺省值补齐并做类型规范化:integer/number/boolean/string/object/array)。脚本直接使用 $ARGS,不必再做类型转换或空值兜底。
- $LISTENER_DATA:由 webRequestListeners 汇聚注入的监听器数据(如认证令牌),键名为对应的 storageKey,通过 $LISTENER_DATA.<key> 访问。
- 禁止使用 ${...} 占位符。
- 严禁将 Response 作为最终返回值;必须使用 $body 将其解析为可序列化数据,或在此基础上做变换后返回。

2. 入参与认证

- 将所有可变入参放到 inputSchema.properties,保留/补充 description、default、required(与来源一致时尽量保持一致)。这些信息用于 mergeDefaultValues:default 会在缺省时生效,type 将驱动入参类型规范化。
- 认证从 $LISTENER_DATA 读取(例如 { 'authorization': $LISTENER_DATA.xxxxx })。不要把 token 写死。

3. 请求构造
- GET:用字符串拼接构造查询串(如 '&page=' & $string($ARGS.page))。
- JSON POST:headers['content-type'] = 'application/json',init.body = $jsonStringify({ ... 使用 $ARGS ... })。
- x-www-form-urlencoded:headers['content-type'] = 'application/x-www-form-urlencoded',init.body = 'k=' & $string($ARGS.k) & '&m=' & ...(自行拼接)。

4. 结果变换(保持语义)
- 如果源里有输出变换(以前的 outputSchemaLines),必须保留同样的语义。
- 写法示例:
(
$resp := $fetch('https://api.example.com/xxx', { 'method': 'GET', 'headers': { 'authorization': $LISTENER_DATA.api_token } });
$data := $body($resp, 'json');
/_ 在 $data 上做 JSONata 变换,返回最终对象 _/
$map($data.items, function($v){ {"id": $v.id, "name": $v.name} })
)

5. 输出格式
- 仅输出:{ "customTools": [ { name, description, inputSchema, script, valid? }, ... ] }

- 若需要给出 webRequestListeners,可一并包含,但不要把 token 硬编码到脚本;脚本中只使用 $LISTENER_DATA.xxx。

6. valid 字段
- 含义:控制是否对外暴露该工具。默认省略(视为 true)。
- 当需要暂时禁用某个工具时,添加 "valid": false 即可;native 侧会自动过滤,不会对外暴露或执行。

7. webRequestListeners 配置要点(仅元数据)

- 目的:为脚本提供认证/状态等“元数据”,不采集响应体。
- 字段:每项包含 { id, enabled, urlPattern, field, storageKey };不要使用 headerName。
- 不支持:meta.\* 前缀(如 meta.tabId 等),请勿使用。
- 不需要:methods、phases;扩展会在合适阶段自动提取。
- field 取值(大小写不敏感的名字部分按原样匹配):
- request.method
- request.url
- request.query:paramName
- request.header:Header-Name
- response.statusCode
- response.statusLine
- response.header:Header-Name
- response.mimeType
- response.fromCache
- response.ip
- 存储与使用:提取结果写入 chrome.storage.local[storageKey];脚本内通过 $LISTENER_DATA.<storageKey> 读取。

示例(仅演示监听器结构)
{
"webRequestListeners": [
{
"id": "api_auth",
"enabled": true,
"urlPattern": "https://api.example.com/*",
"field": "request.header:Authorization",
"storageKey": "api_token"
},
{
"id": "status_track",
"enabled": true,
"urlPattern": "https://api.example.com/*",
"field": "response.statusCode",
"storageKey": "api_status"
}
],
"customTools": []
}

示例(POST JSON)
{
"customTools": [
{
"name": "create_issue",
"description": "创建 Issue",
"inputSchema": {
"type": "object",
"properties": {
"title": { "type": "string", "description": "标题" },
"body": { "type": "string", "description": "内容", "default": "" }
},
"required": ["title"]
},
"script": [
"(",
" $resp := $fetch('https://api.github.com/repos/org/repo/issues', {",
" 'method': 'POST',",
" 'headers': { 'content-type': 'application/json', 'authorization': 'Bearer ' & $LISTENER_DATA.github_token },",
" 'body': $jsonStringify({ 'title': $ARGS.title, 'body': $ARGS.body })",
" });",
" $body($resp, 'json')",
")"
]
}
]
}
4 changes: 4 additions & 0 deletions app/chrome-extension/common/message-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ export const BACKGROUND_MESSAGE_TYPES = {
REFRESH_SERVER_STATUS: 'refresh_server_status',
SERVER_STATUS_CHANGED: 'server_status_changed',
INITIALIZE_SEMANTIC_ENGINE: 'initialize_semantic_engine',
// 自定义工具相关消息类型
GET_CUSTOM_TOOLS_CONFIG: 'get_custom_tools_config',
SAVE_CUSTOM_TOOLS_CONFIG: 'save_custom_tools_config',
EXECUTE_CUSTOM_TOOL: 'execute_custom_tool',
} as const;

// Offscreen message types
Expand Down
2 changes: 2 additions & 0 deletions app/chrome-extension/entrypoints/background/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
initializeSemanticEngineIfCached,
} from './semantic-similarity';
import { initStorageManagerListener } from './storage-manager';
import { initRequestInterceptorListener } from './request-interceptor';
import { cleanupModelCache } from '@/utils/semantic-similarity-engine';

/**
Expand All @@ -15,6 +16,7 @@ export default defineBackground(() => {
initNativeHostListener();
initSemanticSimilarityListener();
initStorageManagerListener();
initRequestInterceptorListener();

// Conditionally initialize semantic similarity engine if model cache exists
initializeSemanticEngineIfCached()
Expand Down
24 changes: 24 additions & 0 deletions app/chrome-extension/entrypoints/background/native-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ interface ServerStatus {
isRunning: boolean;
port?: number;
lastUpdated: number;
error?: string; // Add error message field
}

let currentServerStatus: ServerStatus = {
Expand Down Expand Up @@ -130,6 +131,7 @@ export function connectNativeHost(port: number = NATIVE_HOST.DEFAULT_PORT) {
isRunning: true,
port: port,
lastUpdated: Date.now(),
error: undefined, // Clear any previous error
};
await saveServerStatus(currentServerStatus);
broadcastServerStatusChange(currentServerStatus);
Expand All @@ -145,6 +147,28 @@ export function connectNativeHost(port: number = NATIVE_HOST.DEFAULT_PORT) {
console.log(SUCCESS_MESSAGES.SERVER_STOPPED);
} else if (message.type === NativeMessageType.ERROR_FROM_NATIVE_HOST) {
console.error('Error from native host:', message.payload?.message || 'Unknown error');

// If the error is about server startup/operation failure, update server status
const errorMessage = message.payload?.message;
if (
errorMessage &&
(errorMessage.includes('Failed to start server') ||
errorMessage.includes('EADDRINUSE') ||
errorMessage.includes('address already in use'))
) {
currentServerStatus = {
isRunning: false,
port: currentServerStatus.port,
lastUpdated: Date.now(),
error: errorMessage, // Save the error message
};
await saveServerStatus(currentServerStatus);
broadcastServerStatusChange(currentServerStatus);
console.log(
'Background: Server status updated to stopped due to server error:',
errorMessage,
);
}
} else if (message.type === 'file_operation_response') {
// Forward file operation response back to the requesting tool
chrome.runtime.sendMessage(message).catch(() => {
Expand Down
Loading