@xiashe/sdk 0.1.16 → 0.1.18

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,6 +1,6 @@
1
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';
3
+ import { chmod, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
4
4
  import os from 'node:os';
5
5
  import path from 'node:path';
6
6
 
@@ -10,6 +10,15 @@ const MAX_OUTPUT_BYTES = 32 * 1024;
10
10
  const SENSITIVE_OUTPUT = /payment[-_ ]?proof|payment[-_ ]?needed|seller_signature|runtimeToken|authorization\s*:/i;
11
11
  const SENSITIVE_USER_OUTPUT = /payment[-_ ]?proof|payment[-_ ]?needed|seller_signature|runtimeToken|authorization\s*:|trade[_ -]?no|交易(?:流水)?号|out[_ -]?shake|client[_ -]?session|payguard|validateclientparent|agentfamily/i;
12
12
  const DEFAULT_STATE_DIR = path.join(os.homedir(), '.xiashe', 'alipay-402');
13
+ const OFFICIAL_MEDIA_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.webp']);
14
+ const MAX_OFFICIAL_MEDIA_BYTES = 8 * 1024 * 1024;
15
+ const SAFE_OFFICIAL_FAILURE_CODES = new Set([
16
+ 'TRADE_PROGRESS_FAILED',
17
+ 'PAYMENT_NEEDED_REJECTED',
18
+ 'SYSTEM_ERROR',
19
+ 'BUYER_IDENTITY_REJECTED',
20
+ 'WALLET_NOT_READY'
21
+ ]);
13
22
 
14
23
  export class AlipayWalletError extends Error {
15
24
  constructor(code, message, options = {}) {
@@ -176,7 +185,11 @@ function paymentStatusFromOutput(stdout, stderr) {
176
185
  const output = `${stdout}\n${stderr}`;
177
186
  const completed = /支付(?:已)?成功|交易成功|支付已完成|payment\s+(?:was\s+)?(?:successful|completed)|(?:resource|资源).*(?:obtained|success|成功)/i.test(output);
178
187
  const pending = /支付待确认|等待.*确认|待确认|支付处理中|pending(?:\s+confirmation)?|waiting.*confirm/i.test(output);
179
- const failed = /支付失败|交易失败|payment\s+failed/i.test(output);
188
+ // The official CLI sometimes returns a JSON error object with exit code 0.
189
+ // Treat an explicit unsuccessful result as failed so callers retain the
190
+ // normalized failure category instead of misleadingly reporting an unknown
191
+ // wallet state.
192
+ const failed = /支付失败|交易失败|payment\s+failed|"success"\s*:\s*false|"errorCode"\s*:/i.test(output);
180
193
  return {
181
194
  status: completed ? 'completed' : pending ? 'pending_confirmation' : failed ? 'failed' : 'unknown',
182
195
  credential: queryCredentialFromOutput(output),
@@ -189,14 +202,31 @@ function userVisibleWalletOutput(stdout, stderr) {
189
202
  .replace(/\u001B\[[0-?]*[ -/]*[@-~]/g, '')
190
203
  .split(/\r?\n/)
191
204
  .map((line) => line.trim())
192
- .filter((line) => line && !SENSITIVE_USER_OUTPUT.test(line))
205
+ .filter((line) => line && !line.startsWith('MEDIA:') && !SENSITIVE_USER_OUTPUT.test(line))
193
206
  .slice(0, 12);
194
207
  return lines.length ? lines.join('\n').slice(0, 1_200) : null;
195
208
  }
196
209
 
210
+ /** Preserve only an image emitted by the official wallet in this invocation. */
211
+ async function officialWalletMedia(stdout, stderr) {
212
+ const mediaLine = `${stdout}\n${stderr}`.split(/\r?\n/).find((line) => line.trim().startsWith('MEDIA:'));
213
+ if (!mediaLine) return null;
214
+ const candidate = mediaLine.slice(mediaLine.indexOf('MEDIA:') + 'MEDIA:'.length).trim();
215
+ if (!candidate || !path.isAbsolute(candidate) || !OFFICIAL_MEDIA_EXTENSIONS.has(path.extname(candidate).toLowerCase())) {
216
+ return null;
217
+ }
218
+ try {
219
+ const file = await stat(candidate);
220
+ return file.isFile() && file.size > 0 && file.size <= MAX_OFFICIAL_MEDIA_BYTES ? candidate : null;
221
+ } catch {
222
+ return null;
223
+ }
224
+ }
225
+
197
226
  function walletFailureCode(stdout, stderr, exitCode) {
198
227
  const output = `${stdout}\n${stderr}`;
199
228
  if (/TRADE_PROGRESS_FAILED/i.test(output)) return 'wallet_trade_progress_failed';
229
+ if (/payment[_ -]?needed[_ -]?rejected|账单.*(?:被拒绝|拒绝)|bill.*(?:rejected|denied)/i.test(output)) return 'payment_needed_rejected';
200
230
  if (/余额不足|insufficient\s+balance/i.test(output)) return 'insufficient_balance';
201
231
  if (/已过期|过期|expired/i.test(output)) return 'payment_expired';
202
232
  if (/钱包.*(?:未开启|未授权)|wallet.*(?:not\s+enabled|not\s+authorized)/i.test(output)) return 'wallet_not_ready';
@@ -207,6 +237,81 @@ function walletFailureCode(stdout, stderr, exitCode) {
207
237
  return Number.isInteger(exitCode) && exitCode !== 0 ? 'official_cli_nonzero_exit' : 'official_cli_unclassified';
208
238
  }
209
239
 
240
+ /**
241
+ * The official wallet can return useful machine error codes alongside protocol
242
+ * material. Expose only a small allow-list: arbitrary vendor output can carry
243
+ * credentials, URLs, trade identifiers, or PayGuard internals.
244
+ */
245
+ function safeOfficialFailureCode(stdout, stderr) {
246
+ const output = `${stdout}\n${stderr}`;
247
+ const candidate = output.match(/(?:"(?:errorCode|code)"|\b(?:errorCode|code)\b)\s*[:=]\s*["']?([A-Z][A-Z0-9_]{2,80})/i)?.[1]?.toUpperCase() || '';
248
+ return SAFE_OFFICIAL_FAILURE_CODES.has(candidate) ? candidate : null;
249
+ }
250
+
251
+ function paymentFailureDiagnosis(failureCode, stdout, stderr) {
252
+ const officialCode = safeOfficialFailureCode(stdout, stderr);
253
+ const base = {
254
+ code: failureCode,
255
+ stage: 'official_wallet_trade_creation',
256
+ nextStep: '不要重复付款;保留诊断代码和本次 attemptId 以便定位。'
257
+ };
258
+ if (failureCode === 'payment_needed_rejected') {
259
+ return {
260
+ ...base,
261
+ stage: 'alipay_bill_validation',
262
+ nextStep: '核对支付宝平台中该 APPID 的应用公钥、service_id、收款方 2088 seller_id 和线上接入状态;修复后必须创建新订单再测试。',
263
+ ...(officialCode ? { officialCode } : {})
264
+ };
265
+ }
266
+ if (failureCode === 'wallet_runtime_not_supported') {
267
+ return {
268
+ ...base,
269
+ stage: 'agent_runtime_identity',
270
+ nextStep: '当前运行时没有通过支付宝 Agent 身份校验;请换用已被官方钱包识别并已授权的本地 Agent 运行时。',
271
+ ...(officialCode ? { officialCode } : {})
272
+ };
273
+ }
274
+ if (failureCode === 'buyer_identity_rejected') {
275
+ return {
276
+ ...base,
277
+ stage: 'buyer_identity_validation',
278
+ nextStep: '支付宝钱包的买家身份未通过校验;完成钱包授权后,用同一受支持运行时创建新订单。',
279
+ ...(officialCode ? { officialCode } : {})
280
+ };
281
+ }
282
+ if (failureCode === 'wallet_not_ready') {
283
+ return {
284
+ ...base,
285
+ stage: 'wallet_authorization',
286
+ nextStep: '支付宝钱包未完成授权或未启用;先完成官方钱包授权,再创建新订单。',
287
+ ...(officialCode ? { officialCode } : {})
288
+ };
289
+ }
290
+ if (failureCode === 'wallet_network_error') {
291
+ return {
292
+ ...base,
293
+ stage: 'wallet_network',
294
+ nextStep: '钱包或网络请求没有完成;先检查网络,再创建新订单。',
295
+ ...(officialCode ? { officialCode } : {})
296
+ };
297
+ }
298
+ return {
299
+ ...base,
300
+ ...(officialCode ? { officialCode } : {})
301
+ };
302
+ }
303
+
304
+ function paymentFailureUserVisible(diagnosis) {
305
+ const lines = [
306
+ '支付宝付款未创建可确认交易。',
307
+ `诊断代码:${diagnosis.code}`,
308
+ `失败阶段:${diagnosis.stage}`,
309
+ ...(diagnosis.officialCode ? [`官方返回代码:${diagnosis.officialCode}`] : []),
310
+ `下一步:${diagnosis.nextStep}`
311
+ ];
312
+ return lines.join('\n');
313
+ }
314
+
210
315
  function paymentMessage(status) {
211
316
  if (status === 'completed') return '支付宝钱包显示本次付款已完成;正在由服务端确认权益。';
212
317
  if (status === 'pending_confirmation') return '支付宝已发起本次付款确认;请打开支付宝 App 完成确认,然后运行 status。';
@@ -221,6 +326,9 @@ function paymentFailureMessage(failureCode) {
221
326
  if (failureCode === 'wallet_runtime_not_supported') {
222
327
  return '当前 Agent 运行环境不支持支付宝 AI付;未授予付费 Skill 权益。';
223
328
  }
329
+ if (failureCode === 'payment_needed_rejected') {
330
+ return '支付宝端拒绝了本次付款账单,未创建交易、未扣款。请确认该 APPID 已在支付宝完成 AI按量付费的服务注册、密钥配置、技术集成确认与线上验证前置步骤;若这些均已完成,请提交支付宝问题反馈并附上本次发生时间。';
331
+ }
224
332
  return '支付宝官方钱包没有生成可确认的付款状态;未授予付费 Skill 权益。';
225
333
  }
226
334
 
@@ -259,6 +367,7 @@ function publicPaymentResult(state, status, diagnostic, options = {}) {
259
367
  const commandFailed = options.commandFailed === true;
260
368
  const failureCode = options.failureCode || 'official_cli_nonzero_exit';
261
369
  const failed = status === 'failed' || status === 'wallet_command_failed';
370
+ const diagnosis = failed ? options.diagnosis : null;
262
371
  return {
263
372
  ok: !failed,
264
373
  status,
@@ -267,7 +376,12 @@ function publicPaymentResult(state, status, diagnostic, options = {}) {
267
376
  walletInvoked: true,
268
377
  message: failed ? paymentFailureMessage(failureCode) : paymentMessage(status),
269
378
  diagnostic,
270
- ...(options.userVisible ? { userVisible: options.userVisible } : {}),
379
+ ...(failed && diagnosis ? { userVisible: paymentFailureUserVisible(diagnosis), diagnosis } : {}),
380
+ ...(!failed && options.userVisible ? { userVisible: options.userVisible } : {}),
381
+ ...(options.officialMedia ? { officialMedia: options.officialMedia } : {}),
382
+ ...(status === 'pending_confirmation'
383
+ ? { confirmation: { source: 'official_alipay_wallet', surface: 'app_or_official_media', required: true } }
384
+ : { confirmation: { source: 'official_alipay_wallet', required: false } }),
271
385
  ...(commandFailed ? { failureCode } : {})
272
386
  };
273
387
  }
@@ -341,6 +455,10 @@ export async function payAlipay402Resource(input, options = {}) {
341
455
 
342
456
  const walletInvocation = await runOfficialCli([
343
457
  '402-buyer-pay',
458
+ // The official buyer CLI accepts this long form of -s/-f/-r. The file
459
+ // name is intentionally private and random; the exact Payment-Needed
460
+ // content, original resource URL, and generated UUID session are what
461
+ // define the payment request.
344
462
  '--session-id', state.sessionId,
345
463
  '--file', state.paymentNeededPath,
346
464
  '--resource-url', resourceUrl,
@@ -349,18 +467,24 @@ export async function payAlipay402Resource(input, options = {}) {
349
467
  const result = paymentStatusFromOutput(walletInvocation.stdout, walletInvocation.stderr);
350
468
  const commandFailed = walletInvocation.exitCode !== 0 && result.status === 'unknown';
351
469
  const status = commandFailed ? 'wallet_command_failed' : result.status;
470
+ const officialMedia = ['pending_confirmation', 'completed'].includes(status)
471
+ ? await officialWalletMedia(walletInvocation.stdout, walletInvocation.stderr)
472
+ : null;
352
473
  state.status = status;
353
474
  state.queryCredential = result.credential;
354
475
  state.updatedAt = new Date().toISOString();
355
476
  await writePaymentState(state, options);
356
477
  if (status === 'completed') await rm(paths.paymentNeededPath, { force: true });
357
478
  const failed = commandFailed || status === 'failed';
479
+ const failureCode = failed ? walletFailureCode(walletInvocation.stdout, walletInvocation.stderr, walletInvocation.exitCode) : undefined;
358
480
  return publicPaymentResult(state, status, result.diagnostic, {
359
481
  commandFailed: failed,
360
- failureCode: failed ? walletFailureCode(walletInvocation.stdout, walletInvocation.stderr, walletInvocation.exitCode) : undefined,
482
+ failureCode,
483
+ diagnosis: failed ? paymentFailureDiagnosis(failureCode, walletInvocation.stdout, walletInvocation.stderr) : undefined,
361
484
  userVisible: ['pending_confirmation', 'completed'].includes(status)
362
485
  ? userVisibleWalletOutput(walletInvocation.stdout, walletInvocation.stderr)
363
- : null
486
+ : null,
487
+ officialMedia
364
488
  });
365
489
  }
366
490
 
@@ -374,6 +498,7 @@ export async function queryAlipay402Resource(input, options = {}) {
374
498
  status: 'no_active_payment',
375
499
  provider: 'alipay-agent-payment',
376
500
  walletInvoked: false,
501
+ confirmation: { source: 'official_alipay_wallet', required: false },
377
502
  message: '没有可查询的本地支付宝付款记录;请重新发起付款,不要把付款凭证粘贴到对话中。',
378
503
  diagnostic: 'payment_state_not_found'
379
504
  };
@@ -389,6 +514,9 @@ export async function queryAlipay402Resource(input, options = {}) {
389
514
  const result = paymentStatusFromOutput(walletInvocation.stdout, walletInvocation.stderr);
390
515
  const commandFailed = walletInvocation.exitCode !== 0 && result.status === 'unknown';
391
516
  const status = commandFailed ? 'wallet_command_failed' : result.status;
517
+ const officialMedia = ['pending_confirmation', 'completed'].includes(status)
518
+ ? await officialWalletMedia(walletInvocation.stdout, walletInvocation.stderr)
519
+ : null;
392
520
  state.status = status;
393
521
  state.queryCredential = result.credential || state.queryCredential;
394
522
  state.updatedAt = new Date().toISOString();
@@ -398,11 +526,14 @@ export async function queryAlipay402Resource(input, options = {}) {
398
526
  await rm(paymentNeededPath, { force: true });
399
527
  }
400
528
  const failed = commandFailed || status === 'failed';
529
+ const failureCode = failed ? walletFailureCode(walletInvocation.stdout, walletInvocation.stderr, walletInvocation.exitCode) : undefined;
401
530
  return publicPaymentResult(state, status, result.diagnostic, {
402
531
  commandFailed: failed,
403
- failureCode: failed ? walletFailureCode(walletInvocation.stdout, walletInvocation.stderr, walletInvocation.exitCode) : undefined,
532
+ failureCode,
533
+ diagnosis: failed ? paymentFailureDiagnosis(failureCode, walletInvocation.stdout, walletInvocation.stderr) : undefined,
404
534
  userVisible: ['pending_confirmation', 'completed'].includes(status)
405
535
  ? userVisibleWalletOutput(walletInvocation.stdout, walletInvocation.stderr)
406
- : null
536
+ : null,
537
+ officialMedia
407
538
  });
408
539
  }
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.16';
9
+ export const VERSION = '0.1.18';
10
10
 
11
11
  export const DEFAULTS = {
12
12
  cn: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiashe/sdk",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "type": "module",
5
5
  "main": "index.mjs",
6
6
  "exports": {