@xiashe/sdk 0.1.10 → 0.1.12
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/alipayWallet.mjs +206 -34
- package/index.mjs +2 -1
- package/package.json +1 -1
package/alipayWallet.mjs
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { execFile } from 'node:child_process';
|
|
3
|
+
import { chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
3
6
|
|
|
4
7
|
const DEFAULT_TIMEOUT_MS = 8 * 60 * 1000;
|
|
8
|
+
const RESOURCE_FETCH_TIMEOUT_MS = 20 * 1000;
|
|
5
9
|
const MAX_OUTPUT_BYTES = 32 * 1024;
|
|
6
10
|
const SENSITIVE_OUTPUT = /payment[-_ ]?proof|payment[-_ ]?needed|seller_signature|runtimeToken|authorization\s*:/i;
|
|
11
|
+
const DEFAULT_STATE_DIR = path.join(os.homedir(), '.xiashe', 'alipay-402');
|
|
7
12
|
|
|
8
13
|
export class AlipayWalletError extends Error {
|
|
9
14
|
constructor(code, message, options = {}) {
|
|
@@ -19,14 +24,14 @@ function safeText(value, maxLength = 240) {
|
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
function digest(value) {
|
|
22
|
-
return createHash('sha256').update(String(value || '')).digest('hex')
|
|
27
|
+
return createHash('sha256').update(String(value || '')).digest('hex');
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
function safeDiagnostic(value) {
|
|
26
31
|
const text = safeText(value, MAX_OUTPUT_BYTES);
|
|
27
32
|
if (!text) return 'no_wallet_output';
|
|
28
|
-
//
|
|
29
|
-
//
|
|
33
|
+
// Vendor output is not a stable public API. Never forward it because a new
|
|
34
|
+
// version could include a protocol proof or another credential.
|
|
30
35
|
return SENSITIVE_OUTPUT.test(text) ? 'protocol_payload_processed' : 'official_wallet_response_received';
|
|
31
36
|
}
|
|
32
37
|
|
|
@@ -51,6 +56,48 @@ function normalizeIntentSummary(value) {
|
|
|
51
56
|
return summary;
|
|
52
57
|
}
|
|
53
58
|
|
|
59
|
+
function stateDirectory(options = {}) {
|
|
60
|
+
return path.resolve(options.stateDir || process.env.XIASHE_ALIPAY_PAYMENT_STATE_DIR || DEFAULT_STATE_DIR);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function statePaths(resourceUrl, options = {}) {
|
|
64
|
+
const base = digest(resourceUrl);
|
|
65
|
+
const directory = stateDirectory(options);
|
|
66
|
+
return {
|
|
67
|
+
directory,
|
|
68
|
+
paymentNeededPath: path.join(directory, `${base}.payment-needed`),
|
|
69
|
+
statePath: path.join(directory, `${base}.json`)
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function ensurePrivateDirectory(directory) {
|
|
74
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
75
|
+
// mkdir does not change permissions of an existing directory.
|
|
76
|
+
await chmod(directory, 0o700);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function writePrivateFile(filePath, contents) {
|
|
80
|
+
await writeFile(filePath, contents, { encoding: 'utf8', mode: 0o600 });
|
|
81
|
+
await chmod(filePath, 0o600);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function readPaymentState(resourceUrl, options = {}) {
|
|
85
|
+
const { statePath } = statePaths(resourceUrl, options);
|
|
86
|
+
try {
|
|
87
|
+
const parsed = JSON.parse(await readFile(statePath, 'utf8'));
|
|
88
|
+
return parsed && parsed.version === 1 && parsed.resourceDigest === digest(resourceUrl) ? parsed : null;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (error?.code === 'ENOENT') return null;
|
|
91
|
+
throw new AlipayWalletError('ALIPAY_PAYMENT_STATE_INVALID', '本地支付宝付款状态无法读取;请重新发起本次付款。');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function writePaymentState(state, options = {}) {
|
|
96
|
+
const { directory, statePath } = statePaths(state.resourceUrl, options);
|
|
97
|
+
await ensurePrivateDirectory(directory);
|
|
98
|
+
await writePrivateFile(statePath, `${JSON.stringify(state)}\n`);
|
|
99
|
+
}
|
|
100
|
+
|
|
54
101
|
function defaultRunFile(command, args, options) {
|
|
55
102
|
return new Promise((resolve, reject) => {
|
|
56
103
|
execFile(command, args, {
|
|
@@ -91,9 +138,9 @@ async function runOfficialCli(args, options = {}) {
|
|
|
91
138
|
throw new AlipayWalletError('ALIPAY_BOT_NOT_INSTALLED', '未找到支付宝官方钱包工具。请先在当前 Agent 环境安装并授权 @alipay/agent-payment。');
|
|
92
139
|
}
|
|
93
140
|
if (error?.killed || error?.signal === 'SIGTERM') {
|
|
94
|
-
throw new AlipayWalletError('ALIPAY_WALLET_TIMEOUT', '
|
|
141
|
+
throw new AlipayWalletError('ALIPAY_WALLET_TIMEOUT', '支付宝钱包等待确认超时;请用同一支付命令的 status 查询状态。');
|
|
95
142
|
}
|
|
96
|
-
throw new AlipayWalletError('ALIPAY_WALLET_COMMAND_FAILED', '
|
|
143
|
+
throw new AlipayWalletError('ALIPAY_WALLET_COMMAND_FAILED', '支付宝官方钱包未完成该操作。');
|
|
97
144
|
}
|
|
98
145
|
}
|
|
99
146
|
|
|
@@ -104,10 +151,75 @@ function walletStateFromOutput(stdout, stderr) {
|
|
|
104
151
|
return enabled ? 'ready' : unavailable ? 'unavailable' : 'unknown';
|
|
105
152
|
}
|
|
106
153
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
154
|
+
function queryCredentialFromOutput(output) {
|
|
155
|
+
const tradeNo = output.match(/(?:"tradeNo"|trade\s*no|交易(?:流水)?号)\s*["::=]\s*["']?([A-Za-z0-9_-]{8,128})/i)?.[1] || null;
|
|
156
|
+
const outShakeNo = output.match(/(?:"outShakeNo"|out[_ -]?shake[_ -]?no|外部(?:摇一摇)?编号)\s*["::=]\s*["']?([A-Za-z0-9_-]{8,128})/i)?.[1] || null;
|
|
157
|
+
return tradeNo ? { type: 'trade_no', value: tradeNo } : outShakeNo ? { type: 'out_shake_no', value: outShakeNo } : null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function paymentStatusFromOutput(stdout, stderr) {
|
|
161
|
+
const output = `${stdout}\n${stderr}`;
|
|
162
|
+
const completed = /支付(?:已)?成功|交易成功|支付已完成|payment\s+(?:was\s+)?(?:successful|completed)|(?:resource|资源).*(?:obtained|success|成功)/i.test(output);
|
|
163
|
+
const pending = /支付待确认|等待.*确认|待确认|支付处理中|pending(?:\s+confirmation)?|waiting.*confirm/i.test(output);
|
|
164
|
+
const failed = /支付失败|交易失败|payment\s+failed/i.test(output);
|
|
165
|
+
return {
|
|
166
|
+
status: completed ? 'completed' : pending ? 'pending_confirmation' : failed ? 'failed' : 'unknown',
|
|
167
|
+
credential: queryCredentialFromOutput(output),
|
|
168
|
+
diagnostic: safeDiagnostic(output)
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function paymentMessage(status) {
|
|
173
|
+
if (status === 'completed') return '支付宝钱包显示本次付款已完成;正在由服务端确认权益。';
|
|
174
|
+
if (status === 'pending_confirmation') return '支付宝已发起本次付款确认;请打开支付宝 App 完成确认,然后运行 status。';
|
|
175
|
+
if (status === 'failed') return '支付宝钱包显示本次付款未完成;未授予付费 Skill 权益。';
|
|
176
|
+
return '支付宝官方钱包已被调用,但尚未返回可确认的付款状态;请运行 status 查询,或重新发起付款。';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function fetchPaymentNeeded(resourceUrl, options = {}) {
|
|
180
|
+
const fetchFn = options.fetch || globalThis.fetch;
|
|
181
|
+
const controller = new AbortController();
|
|
182
|
+
const timeout = setTimeout(() => controller.abort(), options.resourceFetchTimeoutMs || RESOURCE_FETCH_TIMEOUT_MS);
|
|
183
|
+
try {
|
|
184
|
+
const response = await fetchFn(resourceUrl, {
|
|
185
|
+
method: 'GET',
|
|
186
|
+
redirect: 'error',
|
|
187
|
+
headers: { Accept: 'application/json' },
|
|
188
|
+
signal: controller.signal
|
|
189
|
+
});
|
|
190
|
+
if (response.status === 200) return { alreadyPaid: true };
|
|
191
|
+
if (response.status !== 402) {
|
|
192
|
+
throw new AlipayWalletError('ALIPAY_RESOURCE_PAYMENT_NEEDED_FAILED', `支付资源未返回付款请求(HTTP ${response.status})。`, { status: response.status });
|
|
193
|
+
}
|
|
194
|
+
const paymentNeeded = response.headers.get('Payment-Needed');
|
|
195
|
+
if (!paymentNeeded) {
|
|
196
|
+
throw new AlipayWalletError('ALIPAY_RESOURCE_PAYMENT_NEEDED_MISSING', '支付资源未返回有效的支付宝付款请求。');
|
|
197
|
+
}
|
|
198
|
+
return { paymentNeeded };
|
|
199
|
+
} catch (error) {
|
|
200
|
+
if (error instanceof AlipayWalletError) throw error;
|
|
201
|
+
if (error?.name === 'AbortError') {
|
|
202
|
+
throw new AlipayWalletError('ALIPAY_RESOURCE_TIMEOUT', '请求支付宝支付资源超时,请重试。');
|
|
203
|
+
}
|
|
204
|
+
throw new AlipayWalletError('ALIPAY_RESOURCE_REQUEST_FAILED', '无法请求支付宝支付资源,请检查网络后重试。');
|
|
205
|
+
} finally {
|
|
206
|
+
clearTimeout(timeout);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function publicPaymentResult(state, status, diagnostic) {
|
|
211
|
+
return {
|
|
212
|
+
ok: status !== 'failed',
|
|
213
|
+
status,
|
|
214
|
+
provider: 'alipay-agent-payment',
|
|
215
|
+
attemptId: `alipay-402:${state.resourceDigest.slice(0, 16)}`,
|
|
216
|
+
walletInvoked: true,
|
|
217
|
+
message: paymentMessage(status),
|
|
218
|
+
diagnostic
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Runs only the official local wallet client and never exposes its raw output. */
|
|
111
223
|
export async function checkAlipayWallet(options = {}) {
|
|
112
224
|
const { stdout, stderr } = await runOfficialCli(['check-wallet'], options);
|
|
113
225
|
const state = walletStateFromOutput(stdout, stderr);
|
|
@@ -120,9 +232,23 @@ export async function checkAlipayWallet(options = {}) {
|
|
|
120
232
|
};
|
|
121
233
|
}
|
|
122
234
|
|
|
235
|
+
/** Checks that the official buyer CLI is present, without querying wallet state. */
|
|
236
|
+
export async function getAlipayWalletBridgeInfo(options = {}) {
|
|
237
|
+
const { stdout, stderr } = await runOfficialCli(['--version'], options);
|
|
238
|
+
return {
|
|
239
|
+
ok: true,
|
|
240
|
+
state: 'available',
|
|
241
|
+
provider: 'alipay-agent-payment',
|
|
242
|
+
message: '支付宝官方付款桥已可用;付款时将由支付宝 App 请求确认。',
|
|
243
|
+
diagnostic: safeDiagnostic(stdout || stderr)
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
123
247
|
/**
|
|
124
|
-
* Starts
|
|
125
|
-
*
|
|
248
|
+
* Starts a buyer-side 402 payment according to the official sequence:
|
|
249
|
+
* request the resource once, preserve its exact Payment-Needed header only in
|
|
250
|
+
* a private local file, then invoke the official buyer CLI with that file and
|
|
251
|
+
* a stable session id. No protocol data leaves this module.
|
|
126
252
|
*/
|
|
127
253
|
export async function payAlipay402Resource(input, options = {}) {
|
|
128
254
|
if (input?.confirm !== true) {
|
|
@@ -130,36 +256,82 @@ export async function payAlipay402Resource(input, options = {}) {
|
|
|
130
256
|
}
|
|
131
257
|
const resourceUrl = normalizeResourceUrl(input?.resourceUrl);
|
|
132
258
|
const intentSummary = normalizeIntentSummary(input?.intentSummary);
|
|
133
|
-
const
|
|
134
|
-
if (
|
|
135
|
-
|
|
259
|
+
const resource = await fetchPaymentNeeded(resourceUrl, options);
|
|
260
|
+
if (resource.alreadyPaid) {
|
|
261
|
+
const state = (await readPaymentState(resourceUrl, options)) || {
|
|
262
|
+
version: 1,
|
|
263
|
+
resourceUrl,
|
|
264
|
+
resourceDigest: digest(resourceUrl),
|
|
265
|
+
sessionId: null,
|
|
266
|
+
paymentNeededPath: null,
|
|
267
|
+
createdAt: new Date().toISOString(),
|
|
268
|
+
updatedAt: new Date().toISOString()
|
|
269
|
+
};
|
|
270
|
+
return publicPaymentResult(state, 'completed', 'resource_already_available');
|
|
136
271
|
}
|
|
272
|
+
|
|
273
|
+
const paths = statePaths(resourceUrl, options);
|
|
274
|
+
await ensurePrivateDirectory(paths.directory);
|
|
275
|
+
await writePrivateFile(paths.paymentNeededPath, resource.paymentNeeded);
|
|
276
|
+
const state = {
|
|
277
|
+
version: 1,
|
|
278
|
+
resourceUrl,
|
|
279
|
+
resourceDigest: digest(resourceUrl),
|
|
280
|
+
sessionId: randomUUID(),
|
|
281
|
+
paymentNeededPath: paths.paymentNeededPath,
|
|
282
|
+
queryCredential: null,
|
|
283
|
+
status: 'started',
|
|
284
|
+
createdAt: new Date().toISOString(),
|
|
285
|
+
updatedAt: new Date().toISOString()
|
|
286
|
+
};
|
|
287
|
+
await writePaymentState(state, options);
|
|
288
|
+
|
|
137
289
|
const { stdout, stderr } = await runOfficialCli([
|
|
138
290
|
'402-buyer-pay',
|
|
291
|
+
'--session-id', state.sessionId,
|
|
292
|
+
'--file', state.paymentNeededPath,
|
|
139
293
|
'--resource-url', resourceUrl,
|
|
140
294
|
'--intent-summary', intentSummary
|
|
141
295
|
], options);
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
};
|
|
296
|
+
const result = paymentStatusFromOutput(stdout, stderr);
|
|
297
|
+
state.status = result.status;
|
|
298
|
+
state.queryCredential = result.credential;
|
|
299
|
+
state.updatedAt = new Date().toISOString();
|
|
300
|
+
await writePaymentState(state, options);
|
|
301
|
+
if (result.status === 'completed') await rm(paths.paymentNeededPath, { force: true });
|
|
302
|
+
return publicPaymentResult(state, result.status, result.diagnostic);
|
|
150
303
|
}
|
|
151
304
|
|
|
305
|
+
/** Queries an existing local payment attempt using the official query credential. */
|
|
152
306
|
export async function queryAlipay402Resource(input, options = {}) {
|
|
153
307
|
const resourceUrl = normalizeResourceUrl(input?.resourceUrl);
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
308
|
+
const state = await readPaymentState(resourceUrl, options);
|
|
309
|
+
if (!state?.queryCredential?.value) {
|
|
310
|
+
return {
|
|
311
|
+
ok: false,
|
|
312
|
+
status: 'no_active_payment',
|
|
313
|
+
provider: 'alipay-agent-payment',
|
|
314
|
+
walletInvoked: false,
|
|
315
|
+
message: '没有可查询的本地支付宝付款记录;请重新发起付款,不要把付款凭证粘贴到对话中。',
|
|
316
|
+
diagnostic: 'payment_state_not_found'
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
const queryArg = state.queryCredential.type === 'out_shake_no'
|
|
320
|
+
? ['--out-shake-no', state.queryCredential.value]
|
|
321
|
+
: ['-t', state.queryCredential.value];
|
|
322
|
+
const { stdout, stderr } = await runOfficialCli([
|
|
323
|
+
'402-query-payment-status',
|
|
324
|
+
...queryArg,
|
|
325
|
+
'--resource-url', resourceUrl
|
|
326
|
+
], options);
|
|
327
|
+
const result = paymentStatusFromOutput(stdout, stderr);
|
|
328
|
+
state.status = result.status;
|
|
329
|
+
state.queryCredential = result.credential || state.queryCredential;
|
|
330
|
+
state.updatedAt = new Date().toISOString();
|
|
331
|
+
await writePaymentState(state, options);
|
|
332
|
+
if (result.status === 'completed') {
|
|
333
|
+
const { paymentNeededPath } = statePaths(resourceUrl, options);
|
|
334
|
+
await rm(paymentNeededPath, { force: true });
|
|
335
|
+
}
|
|
336
|
+
return publicPaymentResult(state, result.status, result.diagnostic);
|
|
165
337
|
}
|
package/index.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import path from 'node:path';
|
|
|
6
6
|
import { ConvexHttpClient } from 'convex/browser';
|
|
7
7
|
import { anyApi as api } from 'convex/server';
|
|
8
8
|
|
|
9
|
-
export const VERSION = '0.1.
|
|
9
|
+
export const VERSION = '0.1.12';
|
|
10
10
|
|
|
11
11
|
export const DEFAULTS = {
|
|
12
12
|
cn: {
|
|
@@ -1361,6 +1361,7 @@ export function stableTaskId() {
|
|
|
1361
1361
|
export {
|
|
1362
1362
|
AlipayWalletError,
|
|
1363
1363
|
checkAlipayWallet,
|
|
1364
|
+
getAlipayWalletBridgeInfo,
|
|
1364
1365
|
payAlipay402Resource,
|
|
1365
1366
|
queryAlipay402Resource
|
|
1366
1367
|
} from './alipayWallet.mjs';
|