@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 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').slice(0, 16);
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
- // CLI output is not a protocol API: never forward it to an Agent. This
29
- // protects future vendor versions even when they add fields we do not know.
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
- * Runs only the official local wallet client. It never returns Payment-Needed,
109
- * Payment-Proof, or raw wallet output to an Agent caller.
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 one explicit buyer-side 402 payment. The official CLI owns the
125
- * protocol payload and App confirmation; callers only receive a safe result.
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 wallet = await checkAlipayWallet(options);
134
- if (!wallet.ok) {
135
- throw new AlipayWalletError('ALIPAY_WALLET_NOT_READY', '支付宝钱包未开启;请先完成钱包授权。');
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
- return {
143
- ok: true,
144
- status: 'submitted',
145
- provider: 'alipay-agent-payment',
146
- attemptId: `alipay-402:${digest(`${resourceUrl}\n${intentSummary}`)}`,
147
- message: '已交给支付宝钱包处理;请在支付宝 App 中确认本次付款。',
148
- diagnostic: safeDiagnostic(stdout || stderr)
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 { stdout, stderr } = await runOfficialCli(['402-query-payment-status', '--resource-url', resourceUrl], options);
155
- const output = `${stdout}\n${stderr}`;
156
- const complete = /success|成功|paid|已支付|completed|完成/i.test(output) && !/pending|等待|处理中/i.test(output);
157
- const pending = /pending|等待|处理中|confirm/i.test(output);
158
- return {
159
- ok: true,
160
- status: complete ? 'completed' : pending ? 'pending_confirmation' : 'unknown',
161
- provider: 'alipay-agent-payment',
162
- message: complete ? '支付宝钱包显示付款已完成。' : pending ? '等待用户在支付宝 App 中确认。' : '支付宝钱包尚未返回可确认的付款状态。',
163
- diagnostic: safeDiagnostic(stdout || stderr)
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.10';
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';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiashe/sdk",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "type": "module",
5
5
  "main": "index.mjs",
6
6
  "exports": {