@aeon-ai-pay/aigateway 0.1.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/CHANGELOG.md +60 -0
- package/LICENSE +21 -0
- package/README.md +116 -0
- package/bin/cli.mjs +155 -0
- package/docs/env-vars.md +73 -0
- package/docs/exit-codes.md +65 -0
- package/docs/ide-setup.md +60 -0
- package/docs/output-schema.md +188 -0
- package/docs/recipes/cron-issue-cards.md +69 -0
- package/docs/recipes/error-recovery.md +53 -0
- package/docs/recipes/integrate-in-agent.md +108 -0
- package/docs/recipes/merchant-integration.md +243 -0
- package/docs/release-process.md +98 -0
- package/docs/troubleshooting.md +200 -0
- package/package.json +58 -0
- package/scripts/postinstall.mjs +40 -0
- package/skills/aigateway/SKILL.md +370 -0
- package/skills/aigateway/references/check-status.md +68 -0
- package/skills/aigateway/references/create-card.md +114 -0
- package/skills/aigateway/references/store.md +87 -0
- package/skills/aigateway/references/x402-protocol.md +143 -0
- package/src/balance.mjs +92 -0
- package/src/commands/clean.mjs +65 -0
- package/src/commands/create-card-status.mjs +67 -0
- package/src/commands/create-card.mjs +333 -0
- package/src/commands/create-image.mjs +428 -0
- package/src/commands/wallet-balance.mjs +47 -0
- package/src/commands/wallet-gas.mjs +99 -0
- package/src/commands/wallet-init.mjs +42 -0
- package/src/commands/wallet-topup.mjs +221 -0
- package/src/commands/wallet-withdraw.mjs +183 -0
- package/src/config.mjs +50 -0
- package/src/constants.mjs +22 -0
- package/src/error-codes.mjs +50 -0
- package/src/funding.mjs +216 -0
- package/src/output.mjs +85 -0
- package/src/sanitize.mjs +48 -0
- package/src/update-check.mjs +69 -0
- package/src/walletconnect.mjs +712 -0
- package/src/x402.mjs +120 -0
- package/templates/cline/.clinerules +53 -0
- package/templates/codex/AGENTS.md +56 -0
- package/templates/cursor/.cursor/rules/aigateway.mdc +60 -0
- package/templates/windsurf/.windsurfrules +48 -0
package/src/funding.mjs
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Funding flow: WalletConnect-based USDT/BNB transfer to session key,
|
|
3
|
+
* plus on-chain pre-authorization (ERC20.approve facilitator).
|
|
4
|
+
*
|
|
5
|
+
* Shared by: create-image (lazy top-up when balance is short),
|
|
6
|
+
* prepare (proactive pre-flight before any image work).
|
|
7
|
+
*/
|
|
8
|
+
import {
|
|
9
|
+
withWallet,
|
|
10
|
+
requestERC20Transfer,
|
|
11
|
+
requestNativeTransfer,
|
|
12
|
+
setStatus,
|
|
13
|
+
} from "./walletconnect.mjs";
|
|
14
|
+
import { BSC_RPC_URL, USDT_BSC, FACILITATOR_ADDRESS } from "./constants.mjs";
|
|
15
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
16
|
+
import {
|
|
17
|
+
createPublicClient,
|
|
18
|
+
createWalletClient,
|
|
19
|
+
http,
|
|
20
|
+
maxUint256,
|
|
21
|
+
} from "viem";
|
|
22
|
+
import { bsc } from "viem/chains";
|
|
23
|
+
import { createInterface } from "node:readline/promises";
|
|
24
|
+
import { logInfo } from "./output.mjs";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Two distinct USDT thresholds — keep them apart:
|
|
28
|
+
* LOW_BALANCE_THRESHOLD: when does prepare *trigger* a top-up?
|
|
29
|
+
* If balance ≥ this, prepare exits ready immediately. Set low (1 USDT, ≈ 50
|
|
30
|
+
* image generations at current pricing) so users aren't asked to refund
|
|
31
|
+
* while they still have plenty of headroom.
|
|
32
|
+
* MIN_TOPUP_USDT: when a top-up *does* happen, what's the minimum amount?
|
|
33
|
+
* A top-up always costs ≥ 5 USDT so a single funding lasts a long time.
|
|
34
|
+
*/
|
|
35
|
+
export const LOW_BALANCE_THRESHOLD = 1;
|
|
36
|
+
export const MIN_TOPUP_USDT = 5;
|
|
37
|
+
export const TOPUP_PRESETS = [5, 10, 20, 50];
|
|
38
|
+
export const AUTO_GAS_BNB = "0.0003";
|
|
39
|
+
|
|
40
|
+
const ERC20_APPROVE_ABI = [
|
|
41
|
+
{
|
|
42
|
+
name: "approve",
|
|
43
|
+
type: "function",
|
|
44
|
+
stateMutability: "nonpayable",
|
|
45
|
+
inputs: [
|
|
46
|
+
{ name: "spender", type: "address" },
|
|
47
|
+
{ name: "amount", type: "uint256" },
|
|
48
|
+
],
|
|
49
|
+
outputs: [{ name: "", type: "bool" }],
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Interactive top-up amount picker (TTY only).
|
|
55
|
+
* Presets ≥ minTopup are offered; custom amount must also be ≥ minTopup.
|
|
56
|
+
*
|
|
57
|
+
* @param {number} minTopup - floor amount in USDT (typically MIN_TOPUP_USDT, but
|
|
58
|
+
* may be higher when shortfall > 5)
|
|
59
|
+
* @returns {Promise<string>} chosen USDT amount as a numeric string
|
|
60
|
+
*/
|
|
61
|
+
export async function promptTopupAmount(minTopup) {
|
|
62
|
+
const presets = TOPUP_PRESETS.filter((v) => v >= minTopup);
|
|
63
|
+
const customIdx = presets.length + 1;
|
|
64
|
+
|
|
65
|
+
logInfo("");
|
|
66
|
+
logInfo(`Choose top-up amount (minimum ${minTopup} USDT):`);
|
|
67
|
+
presets.forEach((v, i) => {
|
|
68
|
+
logInfo(` ${i + 1}) ${v} USDT`);
|
|
69
|
+
});
|
|
70
|
+
logInfo(` ${customIdx}) Custom amount`);
|
|
71
|
+
|
|
72
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
73
|
+
try {
|
|
74
|
+
while (true) {
|
|
75
|
+
const ans = (await rl.question(`Enter choice [1-${customIdx}]: `)).trim();
|
|
76
|
+
const n = Number(ans);
|
|
77
|
+
if (Number.isInteger(n) && n >= 1 && n <= presets.length) {
|
|
78
|
+
return String(presets[n - 1]);
|
|
79
|
+
}
|
|
80
|
+
if (Number.isInteger(n) && n === customIdx) {
|
|
81
|
+
const custom = (await rl.question(`Enter USDT amount (>= ${minTopup}): `)).trim();
|
|
82
|
+
const cn = Number(custom);
|
|
83
|
+
if (!Number.isFinite(cn) || cn <= 0) {
|
|
84
|
+
logInfo("Invalid amount, please retry.");
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (cn < minTopup) {
|
|
88
|
+
logInfo(`Amount must be at least ${minTopup} USDT.`);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
return custom;
|
|
92
|
+
}
|
|
93
|
+
logInfo("Invalid choice, please retry.");
|
|
94
|
+
}
|
|
95
|
+
} finally {
|
|
96
|
+
rl.close();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Open WalletConnect QR, transfer USDT (optional) and/or 0.0003 BNB (optional)
|
|
102
|
+
* from the user's main wallet to the local session key.
|
|
103
|
+
*
|
|
104
|
+
* @param {object} params
|
|
105
|
+
* @param {string} params.sessionAddress - destination (local session key)
|
|
106
|
+
* @param {string|null} params.usdtAmount - USDT amount to transfer, or null to skip
|
|
107
|
+
* @param {boolean} params.needGas - whether to also transfer 0.0003 BNB for approve gas
|
|
108
|
+
*/
|
|
109
|
+
export async function fundSessionKey({ sessionAddress, usdtAmount, needGas }) {
|
|
110
|
+
const pageAmount = usdtAmount || (needGas ? AUTO_GAS_BNB : null);
|
|
111
|
+
const pageToken = usdtAmount ? "USDT" : "BNB";
|
|
112
|
+
const pageGasAmount = (needGas && usdtAmount) ? AUTO_GAS_BNB : null;
|
|
113
|
+
await withWallet({ amount: pageAmount, token: pageToken, gasAmount: pageGasAmount }, async ({ signClient, session, peerAddress }) => {
|
|
114
|
+
const publicClient = createPublicClient({
|
|
115
|
+
chain: bsc,
|
|
116
|
+
transport: http(BSC_RPC_URL, { timeout: 15000, retryCount: 2 }),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (usdtAmount) {
|
|
120
|
+
setStatus("signing", { amount: usdtAmount, token: "USDT", to: sessionAddress });
|
|
121
|
+
logInfo(`\nRequesting USDT transfer: ${usdtAmount} USDT → ${sessionAddress}`);
|
|
122
|
+
logInfo("Please confirm the transaction in your wallet app...");
|
|
123
|
+
|
|
124
|
+
const usdtTxHash = await requestERC20Transfer(signClient, session, {
|
|
125
|
+
from: peerAddress,
|
|
126
|
+
to: sessionAddress,
|
|
127
|
+
token: USDT_BSC,
|
|
128
|
+
amount: usdtAmount,
|
|
129
|
+
decimals: 18,
|
|
130
|
+
});
|
|
131
|
+
setStatus("tx_submitted", { txHash: usdtTxHash, amount: usdtAmount, token: "USDT" });
|
|
132
|
+
logInfo(`USDT transfer submitted: ${usdtTxHash}`);
|
|
133
|
+
logInfo("Waiting for confirmation...");
|
|
134
|
+
|
|
135
|
+
const receipt = await publicClient.waitForTransactionReceipt({
|
|
136
|
+
hash: usdtTxHash,
|
|
137
|
+
timeout: 60_000,
|
|
138
|
+
});
|
|
139
|
+
if (receipt.status !== "success") {
|
|
140
|
+
throw new Error("USDT transfer transaction reverted");
|
|
141
|
+
}
|
|
142
|
+
logInfo("USDT transfer confirmed.");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (needGas) {
|
|
146
|
+
try {
|
|
147
|
+
const activeSessions = signClient.session.getAll();
|
|
148
|
+
const sessionAlive = activeSessions.some((s) => s.topic === session.topic);
|
|
149
|
+
if (!sessionAlive) {
|
|
150
|
+
throw new Error("WalletConnect session expired between USDT and BNB transfers. Run 'aigateway wallet-gas' to add BNB manually.");
|
|
151
|
+
}
|
|
152
|
+
} catch (e) {
|
|
153
|
+
if (e.message.includes("session expired")) throw e;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
setStatus("signing", { amount: AUTO_GAS_BNB, token: "BNB", to: sessionAddress });
|
|
157
|
+
logInfo(`\nRequesting BNB transfer: ${AUTO_GAS_BNB} BNB → ${sessionAddress} (for approve gas)`);
|
|
158
|
+
logInfo("Please confirm the transaction in your wallet app...");
|
|
159
|
+
const bnbTxHash = await requestNativeTransfer(signClient, session, {
|
|
160
|
+
from: peerAddress,
|
|
161
|
+
to: sessionAddress,
|
|
162
|
+
value: AUTO_GAS_BNB,
|
|
163
|
+
});
|
|
164
|
+
setStatus("tx_submitted", { txHash: bnbTxHash, amount: AUTO_GAS_BNB, token: "BNB" });
|
|
165
|
+
logInfo(`BNB transfer submitted: ${bnbTxHash}`);
|
|
166
|
+
const bnbReceipt = await publicClient.waitForTransactionReceipt({
|
|
167
|
+
hash: bnbTxHash,
|
|
168
|
+
timeout: 60_000,
|
|
169
|
+
});
|
|
170
|
+
if (bnbReceipt.status !== "success") {
|
|
171
|
+
throw new Error("BNB transfer reverted");
|
|
172
|
+
}
|
|
173
|
+
logInfo("BNB transfer confirmed.");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
setStatus("confirmed", { token: usdtAmount ? "USDT" : "BNB" });
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Pre-authorize the x402 facilitator to spend session key's USDT (MaxUint256).
|
|
182
|
+
* Session key signs and broadcasts directly — needs BNB for gas.
|
|
183
|
+
*
|
|
184
|
+
* @param {`0x${string}`} privateKey - session key private key
|
|
185
|
+
* @returns {Promise<string>} approve tx hash
|
|
186
|
+
*/
|
|
187
|
+
export async function approveFacilitator(privateKey) {
|
|
188
|
+
const account = privateKeyToAccount(privateKey);
|
|
189
|
+
const walletClient = createWalletClient({
|
|
190
|
+
account,
|
|
191
|
+
chain: bsc,
|
|
192
|
+
transport: http(BSC_RPC_URL, { timeout: 15000, retryCount: 2 }),
|
|
193
|
+
});
|
|
194
|
+
const publicClient = createPublicClient({
|
|
195
|
+
chain: bsc,
|
|
196
|
+
transport: http(BSC_RPC_URL, { timeout: 15000, retryCount: 2 }),
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
logInfo(`Pre-authorizing facilitator from ${account.address}...`);
|
|
200
|
+
const txHash = await walletClient.writeContract({
|
|
201
|
+
address: USDT_BSC,
|
|
202
|
+
abi: ERC20_APPROVE_ABI,
|
|
203
|
+
functionName: "approve",
|
|
204
|
+
args: [FACILITATOR_ADDRESS, maxUint256],
|
|
205
|
+
});
|
|
206
|
+
logInfo(`Approve tx submitted: ${txHash}`);
|
|
207
|
+
const receipt = await publicClient.waitForTransactionReceipt({
|
|
208
|
+
hash: txHash,
|
|
209
|
+
timeout: 60_000,
|
|
210
|
+
});
|
|
211
|
+
if (receipt.status !== "success") {
|
|
212
|
+
throw new Error(`Approve tx reverted: ${txHash}`);
|
|
213
|
+
}
|
|
214
|
+
logInfo("Approve confirmed.");
|
|
215
|
+
return txHash;
|
|
216
|
+
}
|
package/src/output.mjs
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 统一输出封装:envelope JSON + 分级日志
|
|
3
|
+
*
|
|
4
|
+
* stdout: 一行最终 JSON(machine-readable)
|
|
5
|
+
* - 成功:{ ok: true, command, version, data }
|
|
6
|
+
* - 失败:{ ok: false, command, version, error: { code, message, ...context } }
|
|
7
|
+
* stderr: 进度日志(human-readable,agent 可忽略)
|
|
8
|
+
*
|
|
9
|
+
* --legacy-output 模式:保留旧裸字段格式,便于已按旧 JSON 解析的脚本/agent 平滑过渡。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync } from "fs";
|
|
13
|
+
import { join, dirname } from "path";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
15
|
+
import { ERROR_CODES } from "./error-codes.mjs";
|
|
16
|
+
|
|
17
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const VERSION = JSON.parse(
|
|
19
|
+
readFileSync(join(__dirname, "..", "package.json"), "utf-8"),
|
|
20
|
+
).version;
|
|
21
|
+
|
|
22
|
+
let LEGACY_MODE = false;
|
|
23
|
+
let QUIET_MODE = false;
|
|
24
|
+
let VERBOSE_MODE = false;
|
|
25
|
+
|
|
26
|
+
export function setLegacyMode(v) { LEGACY_MODE = !!v; }
|
|
27
|
+
export function setQuietMode(v) { QUIET_MODE = !!v; }
|
|
28
|
+
export function setVerboseMode(v) { VERBOSE_MODE = !!v; }
|
|
29
|
+
export function isLegacyMode() { return LEGACY_MODE; }
|
|
30
|
+
export function isVerboseMode() { return VERBOSE_MODE; }
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 输出成功结果。调用方应在调用后让函数自然返回(不要再 process.exit)。
|
|
34
|
+
* @param {string} command - 命令名,如 "create-card" / "wallet-init"
|
|
35
|
+
* @param {object} data - envelope 模式下放在 data 字段
|
|
36
|
+
* @param {object} [legacyShape] - legacy 模式下直接输出的旧格式对象;省略则使用 data 本身
|
|
37
|
+
*/
|
|
38
|
+
export function emitOk(command, data, legacyShape) {
|
|
39
|
+
if (LEGACY_MODE) {
|
|
40
|
+
console.log(JSON.stringify(legacyShape ?? data, null, 2));
|
|
41
|
+
} else {
|
|
42
|
+
console.log(JSON.stringify({ ok: true, command, version: VERSION, data }));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 输出错误并退出(按错误码对应的 exit 码)。
|
|
48
|
+
* @param {string} command
|
|
49
|
+
* @param {string} code - ERROR_CODES 键名
|
|
50
|
+
* @param {object} [details] - 额外字段。message 字段会覆盖默认 message;legacy 字段在 legacy 模式下完全替代输出
|
|
51
|
+
*/
|
|
52
|
+
export function emitErr(command, code, details = {}) {
|
|
53
|
+
const info = ERROR_CODES[code] || ERROR_CODES.INTERNAL_ERROR;
|
|
54
|
+
const message = details.message || info.message || code;
|
|
55
|
+
const exit = info.exit;
|
|
56
|
+
const { message: _m, legacy, ...rest } = details;
|
|
57
|
+
|
|
58
|
+
if (LEGACY_MODE) {
|
|
59
|
+
const legacyOut = legacy ?? { error: message, ...rest };
|
|
60
|
+
console.error(JSON.stringify(legacyOut));
|
|
61
|
+
} else {
|
|
62
|
+
console.log(JSON.stringify({
|
|
63
|
+
ok: false,
|
|
64
|
+
command,
|
|
65
|
+
version: VERSION,
|
|
66
|
+
error: { code, message, ...rest },
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
process.exit(exit);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 进度日志(quiet 模式压制) */
|
|
73
|
+
export function logInfo(msg) {
|
|
74
|
+
if (!QUIET_MODE) console.error(msg);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** 详细日志(仅 verbose 模式下输出) */
|
|
78
|
+
export function logVerbose(msg) {
|
|
79
|
+
if (VERBOSE_MODE && !QUIET_MODE) console.error(msg);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 错误日志(quiet 模式下也会输出) */
|
|
83
|
+
export function logError(msg) {
|
|
84
|
+
console.error(msg);
|
|
85
|
+
}
|
package/src/sanitize.mjs
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 卡片输出脱敏:隐藏敏感卡片信息(完整卡号→末4位、移除CVV、移除有效期)
|
|
3
|
+
* CLI 输出 JSON 供 Agent 解析,Agent 按产品模板展示给用户
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// 需要替换为末4位的字段
|
|
7
|
+
const CARD_NUMBER_KEYS = new Set([
|
|
8
|
+
"cardnumber", "cardno",
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
// 需要完全移除的字段
|
|
12
|
+
const REMOVE_KEYS = new Set([
|
|
13
|
+
"cvv", "cvv2", "cvc", "cvc2", "securitycode",
|
|
14
|
+
"expiry", "expirydate", "expiredate", "cardexpiry",
|
|
15
|
+
"expirationdate", "validthru",
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 递归脱敏对象:
|
|
20
|
+
* - cardNumber/cardNo → 只保留末4位("•••• 3398")
|
|
21
|
+
* - cvv/securityCode → 移除
|
|
22
|
+
* - expiry/expireDate → 移除
|
|
23
|
+
*/
|
|
24
|
+
export function sanitizeOutput(obj) {
|
|
25
|
+
if (obj === null || obj === undefined) return obj;
|
|
26
|
+
if (Array.isArray(obj)) return obj.map(sanitizeOutput);
|
|
27
|
+
if (typeof obj !== "object") return obj;
|
|
28
|
+
|
|
29
|
+
const result = {};
|
|
30
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
31
|
+
const normalized = key.toLowerCase().replace(/[-_]/g, "");
|
|
32
|
+
|
|
33
|
+
// 移除 CVV、有效期等敏感字段
|
|
34
|
+
if (REMOVE_KEYS.has(normalized)) continue;
|
|
35
|
+
|
|
36
|
+
// 卡号只保留末4位
|
|
37
|
+
if (CARD_NUMBER_KEYS.has(normalized)) {
|
|
38
|
+
if (typeof value === "string" && value.length >= 4) {
|
|
39
|
+
result[key] = "•••• " + value.slice(-4);
|
|
40
|
+
}
|
|
41
|
+
// value 为 null 时不输出此字段
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
result[key] = sanitizeOutput(value);
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 自动版本检查 + 静默后台升级
|
|
3
|
+
*
|
|
4
|
+
* 策略:
|
|
5
|
+
* 1. 同步快速检查(npm view)— 发现新版本时输出提示
|
|
6
|
+
* 2. spawn 后台子进程执行 npm install -g 升级
|
|
7
|
+
* 3. 升级后执行 postinstall.mjs(skills CLI 安装到所有工具)
|
|
8
|
+
* 不阻塞主进程
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
12
|
+
|
|
13
|
+
const PKG_NAME = "@aeon-ai-pay/aigateway";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 启动时调用:同步检查版本 + 后台升级
|
|
17
|
+
* @param {string} currentVersion
|
|
18
|
+
*/
|
|
19
|
+
export function checkForUpdates(currentVersion) {
|
|
20
|
+
// 同步快速检查最新版本(超时短,不阻塞太久)
|
|
21
|
+
let latest;
|
|
22
|
+
try {
|
|
23
|
+
latest = execFileSync("npm", ["view", PKG_NAME, "version"], {
|
|
24
|
+
timeout: 5000,
|
|
25
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
26
|
+
}).toString().trim();
|
|
27
|
+
} catch {
|
|
28
|
+
return; // 网络不可用,静默跳过
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!latest || latest === currentVersion) return;
|
|
32
|
+
|
|
33
|
+
// 有新版本:输出提示
|
|
34
|
+
console.error(`[update] ${PKG_NAME} ${currentVersion} → ${latest}, upgrading in background...`);
|
|
35
|
+
|
|
36
|
+
// 后台执行升级(结果写入日志文件)
|
|
37
|
+
const script = `
|
|
38
|
+
const { execFileSync } = require("child_process");
|
|
39
|
+
const { join } = require("path");
|
|
40
|
+
const { appendFileSync, mkdirSync } = require("fs");
|
|
41
|
+
const { homedir } = require("os");
|
|
42
|
+
const pkg = ${JSON.stringify(PKG_NAME)};
|
|
43
|
+
const ver = ${JSON.stringify(latest)};
|
|
44
|
+
const logDir = join(homedir(), ".aigateway");
|
|
45
|
+
const logFile = join(logDir, "update.log");
|
|
46
|
+
function log(msg) {
|
|
47
|
+
try {
|
|
48
|
+
mkdirSync(logDir, { recursive: true });
|
|
49
|
+
appendFileSync(logFile, new Date().toISOString() + " " + msg + "\\n");
|
|
50
|
+
} catch {}
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
log("Upgrading " + pkg + " to " + ver + "...");
|
|
54
|
+
execFileSync("npm", ["install", "-g", pkg + "@" + ver], { timeout: 120000 });
|
|
55
|
+
const root = execFileSync("npm", ["root", "-g"], { timeout: 10000 }).toString().trim();
|
|
56
|
+
const postinstall = join(root, pkg, "scripts", "postinstall.mjs");
|
|
57
|
+
execFileSync("node", [postinstall], { timeout: 30000 });
|
|
58
|
+
log("Upgrade to " + ver + " succeeded.");
|
|
59
|
+
} catch (e) {
|
|
60
|
+
log("Upgrade to " + ver + " failed: " + (e.message || e));
|
|
61
|
+
}
|
|
62
|
+
`;
|
|
63
|
+
|
|
64
|
+
const child = spawn("node", ["-e", script], {
|
|
65
|
+
stdio: "ignore",
|
|
66
|
+
detached: true,
|
|
67
|
+
});
|
|
68
|
+
child.unref();
|
|
69
|
+
}
|