@bolloon/bolloon-agent 0.1.42 → 0.2.1
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/build/icon.icns +0 -0
- package/build/icon.ico +0 -0
- package/build/icon.png +0 -0
- package/build/tray.png +0 -0
- package/build/trayTemplate.png +0 -0
- package/dist/agents/pi-sdk.js +304 -10
- package/dist/electron/config.js +16 -0
- package/dist/electron/dialogs.js +71 -0
- package/dist/electron/first-run.js +129 -0
- package/dist/electron/ipc.js +16 -0
- package/dist/electron/logger.js +77 -0
- package/dist/electron/main.js +60 -0
- package/dist/electron/menu.js +140 -0
- package/dist/electron/paths.js +33 -0
- package/dist/electron/server.js +73 -0
- package/dist/electron/tray.js +73 -0
- package/dist/electron/window.js +69 -0
- package/dist/index.js +6 -3
- package/dist/security/tool-gate.js +4 -0
- package/dist/web/client.js +3767 -2873
- package/dist/web/components/p2p/P2PModal.js +188 -0
- package/dist/web/components/p2p/index.js +264 -226
- package/dist/web/components/p2p/p2p-modal.js +657 -0
- package/dist/web/components/p2p/p2p-tools.js +248 -0
- package/dist/web/server.js +9 -2
- package/dist/web/ui/message-renderer.js +442 -326
- package/dist/web/ui/step-timeline.js +351 -255
- package/package.json +13 -2
package/build/icon.icns
ADDED
|
Binary file
|
package/build/icon.ico
ADDED
|
Binary file
|
package/build/icon.png
ADDED
|
Binary file
|
package/build/tray.png
ADDED
|
Binary file
|
|
Binary file
|
package/dist/agents/pi-sdk.js
CHANGED
|
@@ -963,7 +963,32 @@ class PiAgentSession {
|
|
|
963
963
|
const cmd = String(args.command || '').trim();
|
|
964
964
|
if (!cmd)
|
|
965
965
|
return { success: false, error: 'command 必填' };
|
|
966
|
-
|
|
966
|
+
// 2026-06-19: 支持多种 args 格式
|
|
967
|
+
// 1. JSON 数组: ["checkout", "-b", "branch"] (LLM 偏好)
|
|
968
|
+
// 2. 逗号分隔: "checkout,-b,branch" (旧格式)
|
|
969
|
+
// 3. 字符串: "checkout -b branch" (单参数)
|
|
970
|
+
let argList = [];
|
|
971
|
+
const rawArgs = args.args;
|
|
972
|
+
if (Array.isArray(rawArgs)) {
|
|
973
|
+
argList = rawArgs.map((s) => String(s).trim()).filter(Boolean);
|
|
974
|
+
}
|
|
975
|
+
else if (typeof rawArgs === 'string') {
|
|
976
|
+
const trimmed = rawArgs.trim();
|
|
977
|
+
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
|
978
|
+
// JSON 数组字符串
|
|
979
|
+
try {
|
|
980
|
+
const parsed = JSON.parse(trimmed);
|
|
981
|
+
if (Array.isArray(parsed)) {
|
|
982
|
+
argList = parsed.map((s) => String(s).trim()).filter(Boolean);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
catch { /* fall through to comma split */ }
|
|
986
|
+
}
|
|
987
|
+
if (argList.length === 0) {
|
|
988
|
+
// 逗号分隔 或 单字符串
|
|
989
|
+
argList = trimmed.split(',').map(s => s.trim()).filter(Boolean);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
967
992
|
const timeoutMs = Number(args.timeoutMs) || 30000;
|
|
968
993
|
const result = await shellExec(cmd, argList, { timeoutMs });
|
|
969
994
|
if (result.deniedByGuard) {
|
|
@@ -989,6 +1014,10 @@ class PiAgentSession {
|
|
|
989
1014
|
return await runSelfImproveLoop(goal);
|
|
990
1015
|
}
|
|
991
1016
|
});
|
|
1017
|
+
// 2026-06-24: Wallet + Polymarket + Safe 工具集 (基于 constraint-runtime/src/tools/)
|
|
1018
|
+
// 真实可用实现: ethers v6 + JsonRpcProvider, 默认走 eth.llamarpc.com
|
|
1019
|
+
// agent 可以创建/导入/查询 EVM 钱包, 查余额, 签名, 转账, 部署 Safe 多签钱包
|
|
1020
|
+
this._registerWalletTools();
|
|
992
1021
|
// list_skills 工具: 列出当前 session 已加载的 skills
|
|
993
1022
|
// 加载源: ~/.bolloon/skills/ → <cwd>/.bolloon/skills/ → ~/.boll/skills/
|
|
994
1023
|
this.tools.set('list_skills', {
|
|
@@ -1416,7 +1445,7 @@ class PiAgentSession {
|
|
|
1416
1445
|
description: '跑 vitest 测试. 自动 bail (失败就停). 默认 60s timeout.',
|
|
1417
1446
|
parameters: { pattern: '可选, 文件 glob e.g. "src/agents/pi-sdk.test.ts"', timeoutMs: '可选, 默认 60000' },
|
|
1418
1447
|
execute: async (args) => {
|
|
1419
|
-
const argv = ['vitest', 'run', '--reporter=
|
|
1448
|
+
const argv = ['vitest', 'run', '--reporter=default', '--no-color', '--bail=1'];
|
|
1420
1449
|
if (args.pattern)
|
|
1421
1450
|
argv.push(String(args.pattern));
|
|
1422
1451
|
const timeoutMs = parseInt(String(args.timeoutMs || '60000')) || 60000;
|
|
@@ -1634,10 +1663,13 @@ class PiAgentSession {
|
|
|
1634
1663
|
// M2.4 (2026-06-17): 缓存 tool 定义 — registerTools() 在构造时调一次, 此后不变
|
|
1635
1664
|
if (this.cachedToolDefinitions)
|
|
1636
1665
|
return this.cachedToolDefinitions;
|
|
1637
|
-
const defs = ['
|
|
1666
|
+
const defs = ['可用工具 (name(params) - 简介):'];
|
|
1638
1667
|
for (const tool of this.tools.values()) {
|
|
1639
|
-
|
|
1640
|
-
|
|
1668
|
+
// 2026-06-19: 压缩 tool 定义 — 只显示参数名 (不显示描述, 减少 60% 长度)
|
|
1669
|
+
// 完整 description 在 history 第一轮注入 (getToolDefinitionsFull 调用), 后续轮只看简短
|
|
1670
|
+
// 避免 system prompt 太大导致 minimax 撞 max_tokens 输出空
|
|
1671
|
+
const paramNames = Object.keys(tool.parameters).join(',');
|
|
1672
|
+
defs.push(`- ${tool.name}(${paramNames})`);
|
|
1641
1673
|
}
|
|
1642
1674
|
this.cachedToolDefinitions = defs.join('\n');
|
|
1643
1675
|
return this.cachedToolDefinitions;
|
|
@@ -2995,6 +3027,10 @@ Workspace root folder: ${this.cwd}
|
|
|
2995
3027
|
/\btool\s*=>\s*["'](\w+)["']/,
|
|
2996
3028
|
// 兼容: [TOOL_CALL] 块内 JSON 形式 {"name": "x", "args": {...}}
|
|
2997
3029
|
/\[TOOL_CALL\][\s\S]*?\{\s*"name"\s*:\s*"(\w+)"\s*,\s*"args"\s*:\s*(\{[\s\S]*?\})/i,
|
|
3030
|
+
// 2026-06-19: 兼容 LLM 输出 "tool_name {json_args}" 形式 (单行, 无 name 字段)
|
|
3031
|
+
// 实际输出: write_file {"path":"docs/test.md","content":"..."}
|
|
3032
|
+
// 只在行首/新行匹配, 避免误匹配普通文本里的 "foo {...}"
|
|
3033
|
+
/(?:^|\n)(\w+)\s+(\{[\s\S]*?\})(?=\n|$)/,
|
|
2998
3034
|
// 2026-06-15 修: 兼容 LLM 输出的 XML 格式 <tool_name>...<arg>value</arg>...</tool_name>
|
|
2999
3035
|
// 实际 LLM 习惯: <shell_exec>\n<command>ls</command>\n<args>["-la", "..."]</args>\n</shell_exec>
|
|
3000
3036
|
/<(\w+)>([\s\S]*?)<\/\1>/,
|
|
@@ -3026,15 +3062,28 @@ Workspace root folder: ${this.cwd}
|
|
|
3026
3062
|
else if (rawArgs && /<\w+>[\s\S]*<\/\w+>/.test(rawArgs)) {
|
|
3027
3063
|
// 2026-06-15 修: XML 格式, 解析内嵌子标签 <argname>value</argname>
|
|
3028
3064
|
// 例: <command>ls</command>\n<args>["-la","~/.bolloon/skills"]</args>
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
const
|
|
3065
|
+
// 2026-06-19 修: 也支持 <parameter name="X">value</parameter> 形式 (OpenAI Hermes function_call)
|
|
3066
|
+
const paramRe = /<parameter\s+name=["'](\w+)["']>([\s\S]*?)<\/parameter>/g;
|
|
3067
|
+
let pMatch;
|
|
3068
|
+
while ((pMatch = paramRe.exec(rawArgs)) !== null) {
|
|
3069
|
+
const argName = pMatch[1];
|
|
3070
|
+
const argValue = pMatch[2].trim();
|
|
3034
3071
|
if (argName && argValue) {
|
|
3035
3072
|
args[argName] = argValue;
|
|
3036
3073
|
}
|
|
3037
3074
|
}
|
|
3075
|
+
// 如果没匹配到 <parameter>, 用普通 XML 子标签
|
|
3076
|
+
if (Object.keys(args).length === 0) {
|
|
3077
|
+
const xmlArgPattern = /<(\w+)>([\s\S]*?)<\/\1>/g;
|
|
3078
|
+
let xmlMatch;
|
|
3079
|
+
while ((xmlMatch = xmlArgPattern.exec(rawArgs)) !== null) {
|
|
3080
|
+
const argName = xmlMatch[1];
|
|
3081
|
+
const argValue = xmlMatch[2].trim();
|
|
3082
|
+
if (argName && argValue) {
|
|
3083
|
+
args[argName] = argValue;
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
}
|
|
3038
3087
|
}
|
|
3039
3088
|
else if (rawArgs) {
|
|
3040
3089
|
// 形参串, 形如 key: value, key2: value2
|
|
@@ -3062,6 +3111,251 @@ Workspace root folder: ${this.cwd}
|
|
|
3062
3111
|
console.log('[DBG parseToolCall] result:', JSON.stringify(r), 'content head:', JSON.stringify(content.substring(0, 200)));
|
|
3063
3112
|
return r;
|
|
3064
3113
|
}
|
|
3114
|
+
/**
|
|
3115
|
+
* 2026-06-24: 注册 Wallet + Polymarket + Safe 工具
|
|
3116
|
+
* 真实实现 (ethers v6 + JsonRpcProvider) — agent 可以创建 EVM 钱包, 查余额, 转账, 签名, Polymarket 下单
|
|
3117
|
+
* 工具来源: src/constraint-runtime/src/tools/{WalletTools,PolymarketSDK,SafeSDK}/
|
|
3118
|
+
*/
|
|
3119
|
+
_registerWalletTools() {
|
|
3120
|
+
// === WalletTools (EVM 钱包) ===
|
|
3121
|
+
this.tools.set('wallet_create', {
|
|
3122
|
+
name: 'wallet_create',
|
|
3123
|
+
description: '创建新 EVM 钱包 (BIP-39 12 词助记词 + 私钥 + 地址). 私钥会在响应中返回, agent 应当保存到 ~/.bolloon/wallets/<address>.json',
|
|
3124
|
+
parameters: {},
|
|
3125
|
+
execute: async () => {
|
|
3126
|
+
try {
|
|
3127
|
+
const { createWallet } = await import('../constraint-runtime/dist/tools/WalletTools/createWallet.js').catch(() => import('../constraint-runtime/src/tools/WalletTools/createWallet.js'));
|
|
3128
|
+
const r = await createWallet();
|
|
3129
|
+
return { success: true, output: `✅ 钱包创建成功:\n address: ${r.address}\n privateKey: ${r.privateKey}\n mnemonic: ${r.mnemonic}\n createdAt: ${r.createdAt}\n\n⚠️ 请 agent 立即把私钥保存到 ~/.bolloon/wallets/${r.address}.json` };
|
|
3130
|
+
}
|
|
3131
|
+
catch (e) {
|
|
3132
|
+
return { success: false, error: `创建失败: ${String(e.message || e)}` };
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
});
|
|
3136
|
+
this.tools.set('wallet_import', {
|
|
3137
|
+
name: 'wallet_import',
|
|
3138
|
+
description: '导入已有 EVM 钱包 (用助记词或私钥)',
|
|
3139
|
+
parameters: { mnemonic: '可选, 12/15/18/21/24 词助记词', privateKey: '可选, 0x 开头的私钥' },
|
|
3140
|
+
execute: async (args) => {
|
|
3141
|
+
try {
|
|
3142
|
+
const { importWallet } = await import('../constraint-runtime/dist/tools/WalletTools/importWallet.js').catch(() => import('../constraint-runtime/src/tools/WalletTools/importWallet.js'));
|
|
3143
|
+
const r = await importWallet({ mnemonic: args.mnemonic, privateKey: args.privateKey });
|
|
3144
|
+
return { success: true, output: `✅ 钱包导入成功:\n address: ${r.address}\n privateKey: ${r.privateKey}\n source: ${r.source}` };
|
|
3145
|
+
}
|
|
3146
|
+
catch (e) {
|
|
3147
|
+
return { success: false, error: `导入失败: ${String(e.message || e)}` };
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
});
|
|
3151
|
+
this.tools.set('wallet_get_balance', {
|
|
3152
|
+
name: 'wallet_get_balance',
|
|
3153
|
+
description: '查 EVM 钱包 ETH 余额. 默认 RPC = https://eth.llamarpc.com, 可指定其他 RPC URL',
|
|
3154
|
+
parameters: { address: '0x 开头的 EVM 地址 (必填)', rpcUrl: '可选 RPC URL (默认 eth.llamarpc.com)' },
|
|
3155
|
+
execute: async (args) => {
|
|
3156
|
+
try {
|
|
3157
|
+
const { getBalance } = await import('../constraint-runtime/dist/tools/WalletTools/getBalance.js').catch(() => import('../constraint-runtime/src/tools/WalletTools/getBalance.js'));
|
|
3158
|
+
const r = await getBalance({ address: String(args.address), rpcUrl: args.rpcUrl });
|
|
3159
|
+
return { success: true, output: `💰 ${r.address}\n ${r.balanceEth} ${r.symbol} (${r.balance} wei)` };
|
|
3160
|
+
}
|
|
3161
|
+
catch (e) {
|
|
3162
|
+
return { success: false, error: `查余额失败: ${String(e.message || e)}` };
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
});
|
|
3166
|
+
this.tools.set('wallet_sign_message', {
|
|
3167
|
+
name: 'wallet_sign_message',
|
|
3168
|
+
description: '用私钥对消息做 EIP-191 personal_sign 签名',
|
|
3169
|
+
parameters: { message: '要签名的消息 (必填)', privateKey: '0x 开头的私钥 (必填)' },
|
|
3170
|
+
execute: async (args) => {
|
|
3171
|
+
try {
|
|
3172
|
+
const { signMessage } = await import('../constraint-runtime/dist/tools/WalletTools/signMessage.js').catch(() => import('../constraint-runtime/src/tools/WalletTools/signMessage.js'));
|
|
3173
|
+
const r = await signMessage({ message: String(args.message), privateKey: String(args.privateKey) });
|
|
3174
|
+
return { success: true, output: `✅ 签名完成:\n address: ${r.address}\n message: ${r.message}\n signature: ${r.signature}` };
|
|
3175
|
+
}
|
|
3176
|
+
catch (e) {
|
|
3177
|
+
return { success: false, error: `签名失败: ${String(e.message || e)}` };
|
|
3178
|
+
}
|
|
3179
|
+
}
|
|
3180
|
+
});
|
|
3181
|
+
this.tools.set('wallet_send_tx', {
|
|
3182
|
+
name: 'wallet_send_tx',
|
|
3183
|
+
description: '用 EVM 钱包发送交易 (转账 ETH 或调用合约). 走 EIP-1559, 默认 RPC = eth.llamarpc.com',
|
|
3184
|
+
parameters: { privateKey: '私钥 (必填)', to: '接收地址 (必填)', value: '发送 wei 数量 (必填)', data: '可选 0x 开头的 calldata', rpcUrl: '可选 RPC URL' },
|
|
3185
|
+
execute: async (args) => {
|
|
3186
|
+
try {
|
|
3187
|
+
const { sendTransaction } = await import('../constraint-runtime/dist/tools/WalletTools/sendTransaction.js').catch(() => import('../constraint-runtime/src/tools/WalletTools/sendTransaction.js'));
|
|
3188
|
+
const r = await sendTransaction({
|
|
3189
|
+
privateKey: String(args.privateKey),
|
|
3190
|
+
to: String(args.to),
|
|
3191
|
+
value: String(args.value),
|
|
3192
|
+
data: args.data ? String(args.data) : undefined,
|
|
3193
|
+
rpcUrl: args.rpcUrl,
|
|
3194
|
+
});
|
|
3195
|
+
return { success: true, output: `✅ 交易已发送:\n hash: ${r.hash}\n from: ${r.from} → to: ${r.to}\n value: ${r.value} wei\n status: ${r.status}\n blockNumber: ${r.blockNumber || '(pending)'}\n gasUsed: ${r.gasUsed || '?'}` };
|
|
3196
|
+
}
|
|
3197
|
+
catch (e) {
|
|
3198
|
+
return { success: false, error: `交易失败: ${String(e.message || e)}` };
|
|
3199
|
+
}
|
|
3200
|
+
}
|
|
3201
|
+
});
|
|
3202
|
+
this.tools.set('wallet_transfer_token', {
|
|
3203
|
+
name: 'wallet_transfer_token',
|
|
3204
|
+
description: '用 EVM 钱包转 ERC20 token (需要 token 合约地址)',
|
|
3205
|
+
parameters: { privateKey: '私钥 (必填)', tokenAddress: 'ERC20 合约地址 (必填)', to: '接收地址 (必填)', amount: 'token 数量 (人类可读, 自动按 decimals 转换)', decimals: '可选 token decimals (默认查合约)', rpcUrl: '可选 RPC URL' },
|
|
3206
|
+
execute: async (args) => {
|
|
3207
|
+
try {
|
|
3208
|
+
const { transferToken } = await import('../constraint-runtime/dist/tools/WalletTools/transferToken.js').catch(() => import('../constraint-runtime/src/tools/WalletTools/transferToken.js'));
|
|
3209
|
+
const r = await transferToken({
|
|
3210
|
+
privateKey: String(args.privateKey),
|
|
3211
|
+
tokenAddress: String(args.tokenAddress),
|
|
3212
|
+
to: String(args.to),
|
|
3213
|
+
amount: String(args.amount),
|
|
3214
|
+
decimals: args.decimals ? Number(args.decimals) : undefined,
|
|
3215
|
+
rpcUrl: args.rpcUrl,
|
|
3216
|
+
});
|
|
3217
|
+
return { success: true, output: `✅ Token 转账完成:\n hash: ${r.hash}\n ${r.amount} ${r.tokenAddress.substring(0, 10)}...\n from: ${r.from} → to: ${r.to}\n status: ${r.status}` };
|
|
3218
|
+
}
|
|
3219
|
+
catch (e) {
|
|
3220
|
+
return { success: false, error: `转账失败: ${String(e.message || e)}` };
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
});
|
|
3224
|
+
this.tools.set('wallet_autopay', {
|
|
3225
|
+
name: 'wallet_autopay',
|
|
3226
|
+
description: '设置自动付款 (订阅/定期扣款, 用 SafeSDK 自动执行). 实际能力取决于 SafeSDK autopay 实现',
|
|
3227
|
+
parameters: { from: '付款方地址 (必填)', to: '收款方地址 (必填)', amount: '金额 (必填)', interval: '周期 (e.g. daily/weekly/monthly)', token: '可选, 默认 ETH' },
|
|
3228
|
+
execute: async (args) => {
|
|
3229
|
+
try {
|
|
3230
|
+
const mod = await import('../constraint-runtime/dist/tools/WalletTools/autoPay.js').catch(() => import('../constraint-runtime/src/tools/WalletTools/autoPay.js'));
|
|
3231
|
+
const fn = mod.autoPay || mod.setAutoPay || mod.default;
|
|
3232
|
+
if (!fn)
|
|
3233
|
+
return { success: false, error: 'autoPay 接口未找到' };
|
|
3234
|
+
const r = await fn({
|
|
3235
|
+
from: String(args.from), to: String(args.to), amount: String(args.amount),
|
|
3236
|
+
interval: String(args.interval || 'monthly'), token: args.token,
|
|
3237
|
+
});
|
|
3238
|
+
return { success: true, output: `✅ 自动付款已设置: ${JSON.stringify(r)}` };
|
|
3239
|
+
}
|
|
3240
|
+
catch (e) {
|
|
3241
|
+
return { success: false, error: `设置失败: ${String(e.message || e)}` };
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
});
|
|
3245
|
+
// === PolymarketSDK (预测市场) ===
|
|
3246
|
+
this.tools.set('polymarket_list_markets', {
|
|
3247
|
+
name: 'polymarket_list_markets',
|
|
3248
|
+
description: '列出 Polymarket 预测市场 (用 Polymarket CLOB API). limit 默认 50',
|
|
3249
|
+
parameters: { limit: '可选 数量 (默认 50)', offset: '可选 偏移', closed: '可选 是否只显示已关闭 (默认 false = 只显示活跃)' },
|
|
3250
|
+
execute: async (args) => {
|
|
3251
|
+
try {
|
|
3252
|
+
const { listMarkets } = await import('../constraint-runtime/dist/tools/PolymarketSDK/listMarkets.js').catch(() => import('../constraint-runtime/src/tools/PolymarketSDK/listMarkets.js'));
|
|
3253
|
+
const markets = await listMarkets({
|
|
3254
|
+
limit: args.limit ? Number(args.limit) : 50,
|
|
3255
|
+
offset: args.offset ? Number(args.offset) : 0,
|
|
3256
|
+
closed: args.closed ? String(args.closed) === 'true' : false,
|
|
3257
|
+
});
|
|
3258
|
+
if (!markets || markets.length === 0) {
|
|
3259
|
+
return { success: true, output: '📊 Polymarket 当前没有活跃市场 (或 API 不可达, 因为 constraint-runtime 是占位实现, 需要装 polymarket-sdk 包)' };
|
|
3260
|
+
}
|
|
3261
|
+
const lines = markets.slice(0, 10).map((m, i) => ` ${i + 1}. [${m.id}] ${m.question}\n vol=${m.volume} active=${m.active}`);
|
|
3262
|
+
return { success: true, output: `📊 Polymarket 找到 ${markets.length} 个市场 (前 10):\n${lines.join('\n')}` };
|
|
3263
|
+
}
|
|
3264
|
+
catch (e) {
|
|
3265
|
+
return { success: false, error: `查询失败: ${String(e.message || e)}` };
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
});
|
|
3269
|
+
this.tools.set('polymarket_get_market', {
|
|
3270
|
+
name: 'polymarket_get_market',
|
|
3271
|
+
description: '获取单个 Polymarket 市场的详情 (id/conditionId 都可)',
|
|
3272
|
+
parameters: { marketId: '市场 ID (必填)' },
|
|
3273
|
+
execute: async (args) => {
|
|
3274
|
+
try {
|
|
3275
|
+
const mod = await import('../constraint-runtime/dist/tools/PolymarketSDK/getMarket.js').catch(() => import('../constraint-runtime/src/tools/PolymarketSDK/getMarket.js'));
|
|
3276
|
+
const fn = mod.getMarket || mod.default;
|
|
3277
|
+
const m = await fn(String(args.marketId));
|
|
3278
|
+
if (!m)
|
|
3279
|
+
return { success: false, error: '市场不存在' };
|
|
3280
|
+
return { success: true, output: `📊 市场详情:\n ${JSON.stringify(m, null, 2).substring(0, 1500)}` };
|
|
3281
|
+
}
|
|
3282
|
+
catch (e) {
|
|
3283
|
+
return { success: false, error: `查询失败: ${String(e.message || e)}` };
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
});
|
|
3287
|
+
this.tools.set('polymarket_get_orders', {
|
|
3288
|
+
name: 'polymarket_get_orders',
|
|
3289
|
+
description: '查询 Polymarket 订单 (需要 CLOB client + auth, 当前 constraint-runtime 是占位实现)',
|
|
3290
|
+
parameters: { marketId: '可选 按市场 ID 过滤' },
|
|
3291
|
+
execute: async (args) => {
|
|
3292
|
+
try {
|
|
3293
|
+
const { getOrders } = await import('../constraint-runtime/dist/tools/PolymarketSDK/getOrders.js').catch(() => import('../constraint-runtime/src/tools/PolymarketSDK/getOrders.js'));
|
|
3294
|
+
const orders = await getOrders(args.marketId ? { marketId: String(args.marketId) } : {});
|
|
3295
|
+
return { success: true, output: `📋 订单列表: ${JSON.stringify(orders, null, 2).substring(0, 1500)}` };
|
|
3296
|
+
}
|
|
3297
|
+
catch (e) {
|
|
3298
|
+
return { success: false, error: `查询失败: ${String(e.message || e)}` };
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
});
|
|
3302
|
+
this.tools.set('polymarket_create_order', {
|
|
3303
|
+
name: 'polymarket_create_order',
|
|
3304
|
+
description: '在 Polymarket 下单 (BUY/SELL, price 0-1, size USDC). 当前 constraint-runtime 是占位实现, 实际下单需要 CLOB client + 私钥签名',
|
|
3305
|
+
parameters: { marketId: '市场 ID (必填)', side: 'BUY 或 SELL (必填)', price: '价格 0-1 (必填)', size: '数量 USDC (必填)' },
|
|
3306
|
+
execute: async (args) => {
|
|
3307
|
+
try {
|
|
3308
|
+
const { createOrder } = await import('../constraint-runtime/dist/tools/PolymarketSDK/createOrder.js').catch(() => import('../constraint-runtime/src/tools/PolymarketSDK/createOrder.js'));
|
|
3309
|
+
const r = await createOrder({
|
|
3310
|
+
marketId: String(args.marketId),
|
|
3311
|
+
side: String(args.side).toUpperCase() === 'SELL' ? 'SELL' : 'BUY',
|
|
3312
|
+
price: Number(args.price),
|
|
3313
|
+
size: Number(args.size),
|
|
3314
|
+
});
|
|
3315
|
+
if (r.success)
|
|
3316
|
+
return { success: true, output: `✅ 订单: ${r.message}` };
|
|
3317
|
+
return { success: false, error: r.message, output: r.message };
|
|
3318
|
+
}
|
|
3319
|
+
catch (e) {
|
|
3320
|
+
return { success: false, error: `下单失败: ${String(e.message || e)}` };
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
});
|
|
3324
|
+
this.tools.set('polymarket_cancel_order', {
|
|
3325
|
+
name: 'polymarket_cancel_order',
|
|
3326
|
+
description: '取消 Polymarket 订单 (order ID)',
|
|
3327
|
+
parameters: { orderId: '订单 ID (必填)' },
|
|
3328
|
+
execute: async (args) => {
|
|
3329
|
+
try {
|
|
3330
|
+
const { cancelOrder } = await import('../constraint-runtime/dist/tools/PolymarketSDK/cancelOrder.js').catch(() => import('../constraint-runtime/src/tools/PolymarketSDK/cancelOrder.js'));
|
|
3331
|
+
const r = await cancelOrder({ orderId: String(args.orderId) });
|
|
3332
|
+
if (r.success)
|
|
3333
|
+
return { success: true, output: `✅ 取消订单: ${r.message}` };
|
|
3334
|
+
return { success: false, error: r.message, output: r.message };
|
|
3335
|
+
}
|
|
3336
|
+
catch (e) {
|
|
3337
|
+
return { success: false, error: `取消失败: ${String(e.message || e)}` };
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
});
|
|
3341
|
+
// === SafeSDK (多签钱包) ===
|
|
3342
|
+
this.tools.set('safe_deploy', {
|
|
3343
|
+
name: 'safe_deploy',
|
|
3344
|
+
description: '部署 Safe 多签钱包 (需要 owners/threshold 参数)',
|
|
3345
|
+
parameters: { owners: 'JSON 数组 owner 地址 (必填)', threshold: '需要几个签名 (必填, e.g. 2)' },
|
|
3346
|
+
execute: async (args) => {
|
|
3347
|
+
try {
|
|
3348
|
+
const { deploySafe } = await import('../constraint-runtime/dist/tools/SafeSDK/deploySafe.js').catch(() => import('../constraint-runtime/src/tools/SafeSDK/deploySafe.js'));
|
|
3349
|
+
const owners = Array.isArray(args.owners) ? args.owners : JSON.parse(String(args.owners));
|
|
3350
|
+
const r = await deploySafe({ owners, threshold: Number(args.threshold) });
|
|
3351
|
+
return { success: true, output: `✅ Safe 部署: ${JSON.stringify(r)}` };
|
|
3352
|
+
}
|
|
3353
|
+
catch (e) {
|
|
3354
|
+
return { success: false, error: `部署失败: ${String(e.message || e)}` };
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
});
|
|
3358
|
+
}
|
|
3065
3359
|
/**
|
|
3066
3360
|
* 2026-06-19: 工具名大小写不敏感 + Claude Code 风格别名映射
|
|
3067
3361
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 常量配置 (env 解析在这里集中, 不散在 main 流程)
|
|
3
|
+
*/
|
|
4
|
+
import { app } from 'electron';
|
|
5
|
+
export const DEFAULT_PORT = 54188;
|
|
6
|
+
/** Hard-pin to loopback; LAN exposure must be explicit. */
|
|
7
|
+
export const DEFAULT_HOST = '127.0.0.1';
|
|
8
|
+
export const WEB_SERVER_STARTUP_TIMEOUT_MS = 15_000;
|
|
9
|
+
export const MAIN_WINDOW_DEFAULT = { width: 1200, height: 800 };
|
|
10
|
+
export const MAIN_WINDOW_MIN = { width: 800, height: 600 };
|
|
11
|
+
export function preferredPort() {
|
|
12
|
+
const raw = process.env.ELECTRON_PORT || process.env.PORT;
|
|
13
|
+
const n = parseInt(raw || '', 10);
|
|
14
|
+
return Number.isFinite(n) && n > 0 && n < 65536 ? n : DEFAULT_PORT;
|
|
15
|
+
}
|
|
16
|
+
export const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 文件 dialog 桥 (open / save / dir) + 安全的 fs 桥 (read / write / exists)
|
|
3
|
+
*
|
|
4
|
+
* 5MB read 上限保护 — 渲染进程直接 fs.readFile 没法做限制, 走主进程就有界
|
|
5
|
+
* 所有 handler 解析 event.sender 拿到 window, 让 dialog 模态在该窗口上
|
|
6
|
+
*/
|
|
7
|
+
import { BrowserWindow, dialog, ipcMain } from 'electron';
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import { log } from './logger';
|
|
11
|
+
const MAX_READ_BYTES = 5 * 1024 * 1024; // 5MB
|
|
12
|
+
function windowFor(event) {
|
|
13
|
+
return BrowserWindow.fromWebContents(event.sender);
|
|
14
|
+
}
|
|
15
|
+
function resolveSafe(target) {
|
|
16
|
+
// 不去硬限制路径 — user 给 renderer 暴露 fs 已经信任了, 这里只 normalize
|
|
17
|
+
return path.resolve(target);
|
|
18
|
+
}
|
|
19
|
+
export function registerDialogIpc() {
|
|
20
|
+
ipcMain.handle('dialog:open-file', async (event, opts = {}) => {
|
|
21
|
+
const win = windowFor(event);
|
|
22
|
+
const result = await dialog.showOpenDialog(win, {
|
|
23
|
+
title: opts.title,
|
|
24
|
+
defaultPath: opts.defaultPath,
|
|
25
|
+
filters: opts.filters,
|
|
26
|
+
properties: opts.properties ?? ['openFile'],
|
|
27
|
+
});
|
|
28
|
+
return { canceled: result.canceled, filePaths: result.filePaths };
|
|
29
|
+
});
|
|
30
|
+
ipcMain.handle('dialog:save-file', async (event, opts = {}) => {
|
|
31
|
+
const win = windowFor(event);
|
|
32
|
+
const result = await dialog.showSaveDialog(win, {
|
|
33
|
+
title: opts.title,
|
|
34
|
+
defaultPath: opts.defaultPath,
|
|
35
|
+
filters: opts.filters,
|
|
36
|
+
});
|
|
37
|
+
return { canceled: result.canceled, filePath: result.filePath };
|
|
38
|
+
});
|
|
39
|
+
ipcMain.handle('dialog:open-directory', async (event, opts = {}) => {
|
|
40
|
+
const win = windowFor(event);
|
|
41
|
+
const result = await dialog.showOpenDialog(win, {
|
|
42
|
+
title: opts.title,
|
|
43
|
+
defaultPath: opts.defaultPath,
|
|
44
|
+
properties: ['openDirectory', 'createDirectory'],
|
|
45
|
+
});
|
|
46
|
+
return { canceled: result.canceled, filePaths: result.filePaths };
|
|
47
|
+
});
|
|
48
|
+
ipcMain.handle('fs:read-text-file', async (_event, opts) => {
|
|
49
|
+
const target = resolveSafe(opts.path);
|
|
50
|
+
const stat = fs.statSync(target);
|
|
51
|
+
if (stat.size > MAX_READ_BYTES) {
|
|
52
|
+
throw new Error(`文件过大: ${stat.size} > ${MAX_READ_BYTES} bytes`);
|
|
53
|
+
}
|
|
54
|
+
return fs.readFileSync(target, { encoding: opts.encoding ?? 'utf8' });
|
|
55
|
+
});
|
|
56
|
+
ipcMain.handle('fs:write-text-file', async (_event, opts) => {
|
|
57
|
+
const target = resolveSafe(opts.path);
|
|
58
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
59
|
+
fs.writeFileSync(target, opts.content, { encoding: opts.encoding ?? 'utf8' });
|
|
60
|
+
});
|
|
61
|
+
ipcMain.handle('fs:path-exists', async (_event, opts) => {
|
|
62
|
+
try {
|
|
63
|
+
fs.accessSync(resolveSafe(opts.path));
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
log('dialog + fs IPC handlers registered');
|
|
71
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 首启检测 + 引导浮层
|
|
3
|
+
*
|
|
4
|
+
* 标记文件写在 userData (不是 ~/.bolloon/), 卸载 app 自然清掉
|
|
5
|
+
* 引导窗是父主窗的 modal, frame=false, 透明背景; 关闭时标记写入
|
|
6
|
+
*/
|
|
7
|
+
import { BrowserWindow, app, ipcMain } from 'electron';
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import { firstRunFlagPath, dataDir, logsDir } from './paths';
|
|
11
|
+
import { log } from './logger';
|
|
12
|
+
const OVERLAY_HTML = `
|
|
13
|
+
<!DOCTYPE html>
|
|
14
|
+
<html>
|
|
15
|
+
<head>
|
|
16
|
+
<meta charset="utf-8" />
|
|
17
|
+
<style>
|
|
18
|
+
:root { color-scheme: light dark; }
|
|
19
|
+
body {
|
|
20
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
21
|
+
margin: 0; padding: 32px 28px;
|
|
22
|
+
background: rgba(245,245,247,0.97);
|
|
23
|
+
color: #1d1d1f;
|
|
24
|
+
height: 100vh; box-sizing: border-box;
|
|
25
|
+
}
|
|
26
|
+
@media (prefers-color-scheme: dark) {
|
|
27
|
+
body { background: rgba(28,28,30,0.97); color: #f5f5f7; }
|
|
28
|
+
}
|
|
29
|
+
h1 { font-size: 18px; margin: 0 0 14px; font-weight: 600; }
|
|
30
|
+
p { font-size: 13px; line-height: 1.55; margin: 8px 0; }
|
|
31
|
+
code {
|
|
32
|
+
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
|
33
|
+
font-size: 12px; padding: 1px 5px;
|
|
34
|
+
background: rgba(0,0,0,0.06); border-radius: 3px;
|
|
35
|
+
}
|
|
36
|
+
@media (prefers-color-scheme: dark) {
|
|
37
|
+
code { background: rgba(255,255,255,0.08); }
|
|
38
|
+
}
|
|
39
|
+
.btn {
|
|
40
|
+
display: inline-block; margin-top: 18px; padding: 8px 18px;
|
|
41
|
+
background: #007aff; color: #fff; border: none; border-radius: 6px;
|
|
42
|
+
font-size: 13px; font-weight: 500; cursor: pointer;
|
|
43
|
+
}
|
|
44
|
+
.btn:hover { background: #0066d6; }
|
|
45
|
+
ul { padding-left: 20px; }
|
|
46
|
+
li { font-size: 13px; line-height: 1.7; }
|
|
47
|
+
</style>
|
|
48
|
+
</head>
|
|
49
|
+
<body>
|
|
50
|
+
<h1>欢迎使用 Bolloon Agent</h1>
|
|
51
|
+
<p>你的数据存放在本地, 不上传:</p>
|
|
52
|
+
<ul>
|
|
53
|
+
<li>配置 / 频道 / P2P 密钥: <code id="data"></code></li>
|
|
54
|
+
<li>运行日志: <code id="logs"></code></li>
|
|
55
|
+
</ul>
|
|
56
|
+
<p>所有数据在卸载后仍保留 — 想清理就手动删除 ~/.bolloon/。</p>
|
|
57
|
+
<button class="btn" id="ok">知道了</button>
|
|
58
|
+
<script>
|
|
59
|
+
const ok = document.getElementById('ok');
|
|
60
|
+
ok.addEventListener('click', () => window.electronAPI.markFirstRunSeen());
|
|
61
|
+
document.getElementById('data').textContent = window.electronAPI.getDataPathSync?.() || '';
|
|
62
|
+
document.getElementById('logs').textContent = window.electronAPI.getLogsPathSync?.() || '';
|
|
63
|
+
</script>
|
|
64
|
+
</body>
|
|
65
|
+
</html>
|
|
66
|
+
`;
|
|
67
|
+
export function hasSeenFirstRun() {
|
|
68
|
+
try {
|
|
69
|
+
return fs.existsSync(firstRunFlagPath());
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export function markFirstRunSeen() {
|
|
76
|
+
try {
|
|
77
|
+
fs.mkdirSync(path.dirname(firstRunFlagPath()), { recursive: true });
|
|
78
|
+
fs.writeFileSync(firstRunFlagPath(), new Date().toISOString());
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
log(`写入首启标记失败: ${err.message}`, 'warn');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export function showFirstRunOverlay(parent) {
|
|
85
|
+
return new Promise((resolve) => {
|
|
86
|
+
const overlay = new BrowserWindow({
|
|
87
|
+
parent,
|
|
88
|
+
modal: true,
|
|
89
|
+
frame: false,
|
|
90
|
+
transparent: true,
|
|
91
|
+
width: 520,
|
|
92
|
+
height: 360,
|
|
93
|
+
resizable: false,
|
|
94
|
+
movable: false,
|
|
95
|
+
minimizable: false,
|
|
96
|
+
maximizable: false,
|
|
97
|
+
title: 'Welcome',
|
|
98
|
+
});
|
|
99
|
+
const dataUrl = 'data:text/html;charset=utf-8,' + encodeURIComponent(OVERLAY_HTML);
|
|
100
|
+
overlay.loadURL(dataUrl);
|
|
101
|
+
// 一次性的 IPC handler, 关闭后清掉避免累积
|
|
102
|
+
const handler = () => {
|
|
103
|
+
markFirstRunSeen();
|
|
104
|
+
overlay.close();
|
|
105
|
+
};
|
|
106
|
+
ipcMain.once('first-run:ack', handler);
|
|
107
|
+
overlay.on('closed', () => {
|
|
108
|
+
ipcMain.removeListener('first-run:ack', handler);
|
|
109
|
+
resolve();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/** 注册 IPC handlers (给 preload 桥用) */
|
|
114
|
+
export function registerFirstRunIpc() {
|
|
115
|
+
ipcMain.handle('first-run:seen', () => hasSeenFirstRun());
|
|
116
|
+
ipcMain.handle('first-run:mark-seen', () => { markFirstRunSeen(); });
|
|
117
|
+
// 同步值 (不用 ipcRenderer.invoke 的 await) — 用 exposeInMainWorld 的 sync getter 更顺
|
|
118
|
+
ipcMain.handle('first-run:data-dir', () => dataDir());
|
|
119
|
+
ipcMain.handle('first-run:logs-dir', () => logsDir());
|
|
120
|
+
log('first-run IPC handlers registered');
|
|
121
|
+
}
|
|
122
|
+
/** 包装 — 决定要不要弹 overlay */
|
|
123
|
+
export async function maybeShowFirstRun(parent) {
|
|
124
|
+
if (hasSeenFirstRun())
|
|
125
|
+
return;
|
|
126
|
+
log('首启 — 弹出引导');
|
|
127
|
+
await showFirstRunOverlay(parent);
|
|
128
|
+
app.addRecentDocument(firstRunFlagPath()); // 跟踪最近文档, 让 user 知道有这文件
|
|
129
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IPC handler 集中注册 — version / userData path / open-external (legacy 3 个)
|
|
3
|
+
* dialog/fs 的注册在 dialogs.ts; updater 的注册在 updater.ts
|
|
4
|
+
*/
|
|
5
|
+
import { app, ipcMain, shell } from 'electron';
|
|
6
|
+
import { userDataDir, dataDir } from './paths';
|
|
7
|
+
import { log } from './logger';
|
|
8
|
+
export function registerCoreIpc() {
|
|
9
|
+
ipcMain.handle('get-version', () => app.getVersion());
|
|
10
|
+
ipcMain.handle('get-user-data-path', () => userDataDir());
|
|
11
|
+
ipcMain.handle('get-data-path', () => dataDir()); // 跟 userData 分开, 渲染层要用
|
|
12
|
+
ipcMain.handle('open-external', async (_event, url) => {
|
|
13
|
+
await shell.openExternal(url);
|
|
14
|
+
});
|
|
15
|
+
log('core IPC handlers registered');
|
|
16
|
+
}
|