@tokenbuddy/tokenbuddy 1.0.36 → 1.0.38

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.
Files changed (146) hide show
  1. package/dist/src/buyer-store.d.ts +7 -2
  2. package/dist/src/buyer-store.js +46 -7
  3. package/dist/src/cli.d.ts +1 -0
  4. package/dist/src/cli.js +15 -7
  5. package/dist/src/daemon.d.ts +12 -0
  6. package/dist/src/daemon.js +791 -61
  7. package/dist/src/doctor-diagnostics.js +1 -6
  8. package/dist/src/provider-install.d.ts +2 -2
  9. package/dist/src/provider-install.js +248 -2
  10. package/dist/src/seller-catalog.d.ts +21 -0
  11. package/dist/src/seller-catalog.js +17 -0
  12. package/dist/src/seller-route-planner.d.ts +4 -1
  13. package/dist/src/seller-route-planner.js +3 -0
  14. package/dist/src/seller-routing-strategy.d.ts +3 -0
  15. package/dist/src/terminal-detect.d.ts +1 -1
  16. package/dist/src/terminal-detect.js +3 -2
  17. package/dist/src/workdir.d.ts +10 -0
  18. package/dist/src/workdir.js +26 -0
  19. package/package.json +15 -2
  20. package/static/ui/assets/index-Djfl9tw5.js +271 -0
  21. package/static/ui/assets/index-DkfztCkn.css +1 -0
  22. package/static/ui/index.html +2 -2
  23. package/dist/src/buyer-store.d.ts.map +0 -1
  24. package/dist/src/buyer-store.js.map +0 -1
  25. package/dist/src/clawtip-bootstrap.d.ts.map +0 -1
  26. package/dist/src/clawtip-bootstrap.js.map +0 -1
  27. package/dist/src/cli.d.ts.map +0 -1
  28. package/dist/src/cli.js.map +0 -1
  29. package/dist/src/credit-tracker.d.ts.map +0 -1
  30. package/dist/src/credit-tracker.js.map +0 -1
  31. package/dist/src/daemon.d.ts.map +0 -1
  32. package/dist/src/daemon.js.map +0 -1
  33. package/dist/src/doctor-clawtip-wallet.d.ts.map +0 -1
  34. package/dist/src/doctor-clawtip-wallet.js.map +0 -1
  35. package/dist/src/doctor-diagnostics.d.ts.map +0 -1
  36. package/dist/src/doctor-diagnostics.js.map +0 -1
  37. package/dist/src/index.d.ts.map +0 -1
  38. package/dist/src/index.js.map +0 -1
  39. package/dist/src/init-clawtip-activation.d.ts.map +0 -1
  40. package/dist/src/init-clawtip-activation.js.map +0 -1
  41. package/dist/src/init-payment-options.d.ts.map +0 -1
  42. package/dist/src/init-payment-options.js.map +0 -1
  43. package/dist/src/init-setup.d.ts.map +0 -1
  44. package/dist/src/init-setup.js.map +0 -1
  45. package/dist/src/model-index.d.ts.map +0 -1
  46. package/dist/src/model-index.js.map +0 -1
  47. package/dist/src/package-update.d.ts.map +0 -1
  48. package/dist/src/package-update.js.map +0 -1
  49. package/dist/src/prewarm-cache.d.ts.map +0 -1
  50. package/dist/src/prewarm-cache.js.map +0 -1
  51. package/dist/src/prewarm-scheduler.d.ts.map +0 -1
  52. package/dist/src/prewarm-scheduler.js.map +0 -1
  53. package/dist/src/provider-install.d.ts.map +0 -1
  54. package/dist/src/provider-install.js.map +0 -1
  55. package/dist/src/provider-routing-config.d.ts.map +0 -1
  56. package/dist/src/provider-routing-config.js.map +0 -1
  57. package/dist/src/registry-trust.d.ts.map +0 -1
  58. package/dist/src/registry-trust.js.map +0 -1
  59. package/dist/src/route-failover.d.ts.map +0 -1
  60. package/dist/src/route-failover.js.map +0 -1
  61. package/dist/src/seller-catalog.d.ts.map +0 -1
  62. package/dist/src/seller-catalog.js.map +0 -1
  63. package/dist/src/seller-concurrency-limiter.d.ts.map +0 -1
  64. package/dist/src/seller-concurrency-limiter.js.map +0 -1
  65. package/dist/src/seller-metadata-cache.d.ts.map +0 -1
  66. package/dist/src/seller-metadata-cache.js.map +0 -1
  67. package/dist/src/seller-pool.d.ts.map +0 -1
  68. package/dist/src/seller-pool.js.map +0 -1
  69. package/dist/src/seller-route-planner.d.ts.map +0 -1
  70. package/dist/src/seller-route-planner.js.map +0 -1
  71. package/dist/src/seller-routing-config.d.ts.map +0 -1
  72. package/dist/src/seller-routing-config.js.map +0 -1
  73. package/dist/src/seller-routing-strategy.d.ts.map +0 -1
  74. package/dist/src/seller-routing-strategy.js.map +0 -1
  75. package/dist/src/stream-failover.d.ts.map +0 -1
  76. package/dist/src/stream-failover.js.map +0 -1
  77. package/dist/src/tb-clawtip-proof.d.ts.map +0 -1
  78. package/dist/src/tb-clawtip-proof.js.map +0 -1
  79. package/dist/src/tb-proxyd.d.ts.map +0 -1
  80. package/dist/src/tb-proxyd.js.map +0 -1
  81. package/dist/src/terminal-detect.d.ts.map +0 -1
  82. package/dist/src/terminal-detect.js.map +0 -1
  83. package/dist/src/terminal-image.d.ts.map +0 -1
  84. package/dist/src/terminal-image.js.map +0 -1
  85. package/src/buyer-store.ts +0 -1090
  86. package/src/clawtip-bootstrap.ts +0 -65
  87. package/src/cli.ts +0 -2243
  88. package/src/credit-tracker.ts +0 -295
  89. package/src/daemon.ts +0 -5475
  90. package/src/doctor-clawtip-wallet.ts +0 -95
  91. package/src/doctor-diagnostics.ts +0 -1026
  92. package/src/index.ts +0 -16
  93. package/src/init-clawtip-activation.ts +0 -695
  94. package/src/init-payment-options.ts +0 -373
  95. package/src/init-setup.ts +0 -165
  96. package/src/model-index.ts +0 -278
  97. package/src/package-update.ts +0 -311
  98. package/src/prewarm-cache.ts +0 -485
  99. package/src/prewarm-scheduler.ts +0 -675
  100. package/src/provider-install.ts +0 -1006
  101. package/src/provider-routing-config.ts +0 -410
  102. package/src/registry-trust.ts +0 -51
  103. package/src/route-failover.ts +0 -304
  104. package/src/seller-catalog.ts +0 -505
  105. package/src/seller-concurrency-limiter.ts +0 -161
  106. package/src/seller-metadata-cache.ts +0 -91
  107. package/src/seller-pool.ts +0 -557
  108. package/src/seller-route-planner.ts +0 -513
  109. package/src/seller-routing-config.ts +0 -211
  110. package/src/seller-routing-strategy.ts +0 -362
  111. package/src/stream-failover.ts +0 -152
  112. package/src/tb-clawtip-proof.ts +0 -28
  113. package/src/tb-proxyd.ts +0 -101
  114. package/src/terminal-detect.ts +0 -333
  115. package/src/terminal-image.ts +0 -228
  116. package/static/ui/assets/index-0MVXD7bH.css +0 -1
  117. package/static/ui/assets/index-BVbeDEwq.js +0 -271
  118. package/static/ui/assets/index-BVbeDEwq.js.map +0 -1
  119. package/tests/cli-routing.test.ts +0 -363
  120. package/tests/control-plane-ui-endpoints.test.ts +0 -1630
  121. package/tests/credit-tracker.test.ts +0 -165
  122. package/tests/daemon-413-fallback.test.ts +0 -92
  123. package/tests/daemon-classify.test.ts +0 -452
  124. package/tests/daemon-roles.test.ts +0 -92
  125. package/tests/daemon-trusted-registry-cache.test.ts +0 -132
  126. package/tests/e2e.test.ts +0 -366
  127. package/tests/image-generation-e2e.test.ts +0 -230
  128. package/tests/model-index.test.ts +0 -198
  129. package/tests/package-update.test.ts +0 -147
  130. package/tests/prewarm-cache.test.ts +0 -296
  131. package/tests/prewarm-scheduler.test.ts +0 -367
  132. package/tests/provider-routing-config.test.ts +0 -150
  133. package/tests/registry-trust.test.ts +0 -28
  134. package/tests/route-failover.test.ts +0 -222
  135. package/tests/seller-catalog-413.test.ts +0 -120
  136. package/tests/seller-catalog-utilities.test.ts +0 -124
  137. package/tests/seller-concurrency-limiter.test.ts +0 -83
  138. package/tests/seller-metadata-cache.test.ts +0 -89
  139. package/tests/seller-pool.test.ts +0 -365
  140. package/tests/seller-route-planner.test.ts +0 -312
  141. package/tests/seller-routing-config.test.ts +0 -124
  142. package/tests/seller-routing-strategy.test.ts +0 -167
  143. package/tests/stream-failover.test.ts +0 -52
  144. package/tests/thousand-seller.test.ts +0 -151
  145. package/tests/tokenbuddy.test.ts +0 -4043
  146. package/tsconfig.json +0 -8
package/src/index.ts DELETED
@@ -1,16 +0,0 @@
1
- import { buildCli } from "./cli.js";
2
-
3
- /**
4
- * CLI 入口:构造 commander program、解析 `process.argv`、异步执行。
5
- * 异常通过 `process.exitCode = 1` 透传,避免在 daemon 未启动等情况下刷红 stderr(已通过 `code` 字段分类)。
6
- */
7
- export function run() {
8
- const program = buildCli();
9
- program.parseAsync(process.argv).catch((error: unknown) => {
10
- const commandError = error as { code?: string; exitCode?: number; message?: string };
11
- if (commandError.code !== "tokenbuddy.daemon_not_running") {
12
- console.error(commandError.message || String(error));
13
- }
14
- process.exitCode = commandError.exitCode || 1;
15
- });
16
- }
@@ -1,695 +0,0 @@
1
- import * as p from "@clack/prompts";
2
- import * as fs from "fs";
3
- import * as os from "os";
4
- import * as path from "path";
5
- import { spawn } from "child_process";
6
- import {
7
- inspectOpenClawWalletConfig,
8
- type OpenClawWalletConfigState,
9
- } from "./init-payment-options.js";
10
-
11
- const DEFAULT_POLL_INTERVAL_MS = 2_000;
12
- const SLEEP_SLICE_MS = 200;
13
- const CLAWTIP_MIN_CLI_VERSION = "1.0.4";
14
- const CLAWTIP_MIN_SKILL_VERSION = "1.0.12";
15
-
16
- /**
17
- * 一次 ClawTip 引导支付的输入参数(由上游 service / wallet-bootstrap 解析后传入)。
18
- * 用来写本地 order 文件并触发 `npx @clawtip/clawtip-cli pay` 流程。
19
- */
20
- export interface ClawtipBootstrapPayment {
21
- /** 订单号(也是 order JSON 文件名) */
22
- orderNo: string;
23
- /** 金额(分) */
24
- amountFen: number;
25
- /** 收款方标识,可选 */
26
- payTo?: string;
27
- /** 上游加密负载,可选 */
28
- encryptedData?: string;
29
- /** 上游分配的 indicator(用于切到正确目录) */
30
- indicator: string;
31
- /** 上游 slug,可选 */
32
- slug?: string;
33
- /** skill ID(写入 order 的 `skill-id` 字段) */
34
- skillId?: string;
35
- /** 订单描述,可选 */
36
- description?: string;
37
- /** 资源 URL(写入 order 的 `resource_url` 字段) */
38
- resourceUrl?: string;
39
- }
40
-
41
- /**
42
- * `parseClawtipCliOutput()` 的输出:从 ClawTip CLI 文本输出中提取的结构化字段。
43
- * 字段都为"尽力推断":缺失时为 undefined,由调用方决定如何处理。
44
- */
45
- export interface ParsedClawtipOutput {
46
- /** 鉴权 URL(可被扫码或浏览器打开) */
47
- authUrl?: string;
48
- /** 设备 ID(用于 check-register 轮询) */
49
- clawtipId?: string;
50
- /** QR 图片本地路径 */
51
- mediaPath?: string;
52
- /** 下游/上游的失败信息(已 sanitize 过的中文提示) */
53
- failureMessage?: string;
54
- /** 是否需要用户做钱包授权(含 QR、authUrl、扫码关键字等) */
55
- requiresWalletAuth: boolean;
56
- /** 钱包是否已就绪(register success / tokeninfo / config.json 等关键字) */
57
- walletReady: boolean;
58
- }
59
-
60
- /**
61
- * `waitForClawtipActivationConfirmation()` 的可注入依赖与可调参数。
62
- * 默认实现使用 `@clack/prompts` 的 `cancel`、本机 `~/.openclaw/configs/` 探查和 npx `check-register`。
63
- */
64
- export interface WaitForClawtipActivationOptions {
65
- /** wallet config 探查函数(默认 `inspectOpenClawWalletConfig`) */
66
- inspectWalletConfig?: () => OpenClawWalletConfigState;
67
- /** 外部取消信号(与 SIGINT 监听器互不冲突) */
68
- isCancelled?: () => boolean;
69
- /** check-register 用的 ClawTip 设备 ID(必填才会触发轮询) */
70
- clawtipId?: string;
71
- /** check-register 命令执行函数(默认 `npx @clawtip/clawtip-cli check-register`) */
72
- checkRegister?: (clawtipId: string) => Promise<void>;
73
- /** 取消时的回调(默认 `@clack/prompts.cancel`) */
74
- cancel?: (message?: string) => void;
75
- /** 轮询间隔(毫秒),默认 2000 */
76
- pollIntervalMs?: number;
77
- /** sleep 实现(测试可注入) */
78
- sleep?: (ms: number) => Promise<void>;
79
- }
80
-
81
- /**
82
- * `startClawtipWalletBootstrap()` 的可注入依赖。
83
- */
84
- export interface StartClawtipWalletBootstrapOptions {
85
- /** 注入的 home 目录(默认 `process.env.HOME` / `os.homedir()`) */
86
- home?: string;
87
- /** 注入的 ClawTip CLI 执行函数(默认 `npx @clawtip/clawtip-cli`) */
88
- runClawtipCommand?: (args: string[]) => Promise<string>;
89
- }
90
-
91
- export interface ClawtipProofProviderPayload {
92
- paymentInstructions?: unknown;
93
- quote?: unknown;
94
- }
95
-
96
- /**
97
- * `checkOpenClawRuntime()` 的可注入依赖。
98
- */
99
- export interface CheckOpenClawRuntimeOptions {
100
- /** 注入的 OpenClaw CLI 执行函数(默认 `openclaw --version`) */
101
- runOpenClawCommand?: (args: string[]) => Promise<string>;
102
- }
103
-
104
- function defaultSleep(ms: number): Promise<void> {
105
- return new Promise((resolve) => setTimeout(resolve, ms));
106
- }
107
-
108
- function defaultHomeDir(): string {
109
- return process.env.HOME || os.homedir();
110
- }
111
-
112
- function clawtipOrderFilePath(home: string, indicator: string, orderNo: string): string {
113
- return path.join(home, ".openclaw", "skills", "orders", indicator, `${orderNo}.json`);
114
- }
115
-
116
- function sanitizeClawtipOutput(output: string): string {
117
- return output
118
- .split("\n")
119
- .map((line) => {
120
- if (line.includes("\"u\"") || line.includes("payCredential") || line.includes("access_token")) {
121
- return "<redacted sensitive ClawTip output>";
122
- }
123
- return line;
124
- })
125
- .join("\n");
126
- }
127
-
128
- function isClawtipPayWalletAuthOutput(args: string[], output: string): boolean {
129
- if (!args.includes("pay")) {
130
- return false;
131
- }
132
- const lower = output.toLowerCase();
133
- return lower.includes("authurl")
134
- || output.includes("MEDIA:")
135
- || lower.includes("clawtipid")
136
- || lower.includes("qrcode")
137
- || lower.includes("扫码");
138
- }
139
-
140
- export interface ResolveNpxCommandOptions {
141
- execPath?: string;
142
- envPath?: string;
143
- platform?: NodeJS.Platform;
144
- exists?: (filePath: string) => boolean;
145
- }
146
-
147
- export function resolveNpxCommand(options: ResolveNpxCommandOptions = {}): string {
148
- const platform = options.platform ?? process.platform;
149
- const binaryName = platform === "win32" ? "npx.cmd" : "npx";
150
- const exists = options.exists ?? ((filePath: string) => fs.existsSync(filePath));
151
- const execPath = options.execPath ?? process.execPath;
152
- const envPath = options.envPath ?? process.env.PATH ?? "";
153
- const candidates = [
154
- execPath ? path.join(path.dirname(execPath), binaryName) : undefined,
155
- ...envPath.split(path.delimiter).map((entry) => entry ? path.join(entry, binaryName) : undefined),
156
- "/opt/homebrew/bin/npx",
157
- "/usr/local/bin/npx",
158
- "/usr/bin/npx",
159
- ].filter((candidate): candidate is string => Boolean(candidate));
160
-
161
- for (const candidate of Array.from(new Set(candidates))) {
162
- if (exists(candidate)) {
163
- return candidate;
164
- }
165
- }
166
- return binaryName;
167
- }
168
-
169
- async function runClawtipCli(args: string[]): Promise<string> {
170
- return await new Promise((resolve, reject) => {
171
- const child = spawn(resolveNpxCommand(), args, {
172
- stdio: ["ignore", "pipe", "pipe"],
173
- });
174
- let stdout = "";
175
- let stderr = "";
176
- child.stdout.on("data", (chunk) => {
177
- stdout += String(chunk);
178
- });
179
- child.stderr.on("data", (chunk) => {
180
- stderr += String(chunk);
181
- });
182
- child.on("error", (error) => {
183
- reject(error);
184
- });
185
- child.on("close", (code) => {
186
- const combined = `${stdout}${stderr}`;
187
- if (code === 0) {
188
- resolve(combined);
189
- return;
190
- }
191
- if (isClawtipPayWalletAuthOutput(args, combined)) {
192
- resolve(combined);
193
- return;
194
- }
195
- reject(new Error(`ClawTip command failed with exit ${code}: ${sanitizeClawtipOutput(combined)}`));
196
- });
197
- });
198
- }
199
-
200
- async function runOpenClawCli(args: string[]): Promise<string> {
201
- return await new Promise((resolve, reject) => {
202
- const child = spawn("openclaw", args, {
203
- stdio: ["ignore", "pipe", "pipe"],
204
- });
205
- let stdout = "";
206
- let stderr = "";
207
- child.stdout.on("data", (chunk) => {
208
- stdout += String(chunk);
209
- });
210
- child.stderr.on("data", (chunk) => {
211
- stderr += String(chunk);
212
- });
213
- child.on("error", (error) => {
214
- reject(error);
215
- });
216
- child.on("close", (code) => {
217
- const combined = `${stdout}${stderr}`;
218
- if (code === 0) {
219
- resolve(combined);
220
- return;
221
- }
222
- reject(new Error(`OpenClaw command failed with exit ${code}: ${combined.trim()}`));
223
- });
224
- });
225
- }
226
-
227
- function findValueAfterKeys(output: string, keys: string[]): string | undefined {
228
- for (const line of output.split("\n")) {
229
- for (const key of keys) {
230
- const index = line.indexOf(key);
231
- if (index < 0) {
232
- continue;
233
- }
234
- const raw = line.slice(index + key.length);
235
- const value = raw
236
- .replace(/^[:=\s]+/, "")
237
- .replace(/^["']+|["',\s]+$/g, "")
238
- .trim();
239
- if (value) {
240
- return value;
241
- }
242
- }
243
- }
244
- return undefined;
245
- }
246
-
247
- function lineContainsWalletAuthCue(line: string): boolean {
248
- const lower = line.toLowerCase();
249
- return lower.includes("authurl")
250
- || lower.includes("scan")
251
- || lower.includes("扫码")
252
- || lower.includes("授权")
253
- || lower.includes("qrcode")
254
- || lower.includes("clawtipid")
255
- || lower.includes("safeMonitor".toLowerCase())
256
- || lower.includes("unifiedauthm");
257
- }
258
-
259
- function findUrlInOutput(output: string): string | undefined {
260
- for (const line of output.split("\n")) {
261
- if (line.includes("process.env.CLAWTIP_PAY")
262
- || line.includes("process.env.GET_PUBLIC_KEY")
263
- || line.includes("process.env.QUERY_TOKEN")) {
264
- continue;
265
- }
266
- const matches = line.match(/https?:\/\/\S+/g) || [];
267
- for (const match of matches) {
268
- const value = match.trim().replace(/^["'`([{\s]+|["'`)\]},.\s]+$/g, "");
269
- if (!(value.startsWith("https://") || value.startsWith("http://"))) {
270
- continue;
271
- }
272
- const lower = value.toLowerCase();
273
- if (value.includes("clawtipId")
274
- || lower.includes("/qrcode")
275
- || lower.includes("safemonitor")
276
- || lower.includes("unifiedauthm")
277
- || lineContainsWalletAuthCue(line)) {
278
- return value;
279
- }
280
- }
281
- }
282
- return undefined;
283
- }
284
-
285
- function findMediaPathInOutput(output: string): string | undefined {
286
- const keyedPath = findValueAfterKeys(output, ["MEDIA", "media", "mediaPath", "media_path"]);
287
- if (keyedPath) {
288
- return keyedPath;
289
- }
290
-
291
- for (const line of output.split("\n").reverse()) {
292
- const value = line.trim().replace(/^["'`]+|["'`,\s]+$/g, "");
293
- if (path.isAbsolute(value) && /\.(png|jpe?g|webp)$/i.test(value)) {
294
- return value;
295
- }
296
- }
297
- return undefined;
298
- }
299
-
300
- function findClawtipFailureMessage(output: string): string | undefined {
301
- for (const line of output.split("\n")) {
302
- const trimmed = line.trim();
303
- if (!trimmed) {
304
- continue;
305
- }
306
- if (trimmed.includes("商家信息有误")
307
- || trimmed.includes("商户信息有误")
308
- || trimmed.includes("支付失败")
309
- || trimmed.includes("下单失败")
310
- || trimmed.includes("收付款方账户不能相同")) {
311
- return sanitizeClawtipOutput(trimmed);
312
- }
313
- const returnedMessage = trimmed.match(/^返回消息[::]\s*(.+)$/);
314
- const returnedText = returnedMessage?.[1]?.trim();
315
- if (returnedText && !returnedText.includes("成功")) {
316
- return sanitizeClawtipOutput(trimmed);
317
- }
318
- }
319
- return undefined;
320
- }
321
-
322
- function decodeClawtipValue(value: string): string {
323
- try {
324
- return decodeURIComponent(value);
325
- } catch {
326
- return value;
327
- }
328
- }
329
-
330
- function clawtipQrWorkspaceDir(home: string): string {
331
- return path.join(home, ".openclaw", "workspace", "clawtip", "qrcode");
332
- }
333
-
334
- function findLatestGeneratedQrMediaPath(home: string, createdAfterMs: number): string | undefined {
335
- const qrDir = clawtipQrWorkspaceDir(home);
336
- if (!fs.existsSync(qrDir)) {
337
- return undefined;
338
- }
339
-
340
- const candidates = fs.readdirSync(qrDir)
341
- .filter((fileName) => /^qrcode-.*\.(png|jpe?g|webp)$/i.test(fileName))
342
- .map((fileName) => {
343
- const filePath = path.join(qrDir, fileName);
344
- return {
345
- filePath,
346
- mtimeMs: fs.statSync(filePath).mtimeMs,
347
- };
348
- })
349
- .filter((entry) => entry.mtimeMs >= createdAfterMs - 1_000)
350
- .sort((left, right) => right.mtimeMs - left.mtimeMs);
351
-
352
- return candidates[0]?.filePath;
353
- }
354
-
355
- function extractClawtipIdFromUrl(value: string): string | undefined {
356
- const decoded = decodeClawtipValue(value);
357
- for (const segment of decoded.split(/[?&]/)) {
358
- const [key, rawValue] = segment.split("=", 2);
359
- if (!key || !rawValue) {
360
- continue;
361
- }
362
- if (["clawtipId", "clawtip_id", "deviceId", "device_id"].includes(key)) {
363
- const trimmed = rawValue.trim();
364
- if (trimmed) {
365
- return trimmed;
366
- }
367
- }
368
- }
369
- return undefined;
370
- }
371
-
372
- /**
373
- * 从 ClawTip CLI 的 stdout/stderr 合并文本中提取关键信息。
374
- * 同时识别 authUrl / clawtipId / mediaPath / 失败关键字 / 是否需要钱包授权 / 钱包是否就绪。
375
- *
376
- * @param output 合并后的 ClawTip CLI 输出
377
- * @returns 结构化解析结果
378
- */
379
- export function parseClawtipCliOutput(output: string): ParsedClawtipOutput {
380
- const authUrl = findValueAfterKeys(output, ["authUrl", "auth_url", "auth url"]) || findUrlInOutput(output);
381
- const mediaPath = findMediaPathInOutput(output);
382
- const clawtipId = authUrl
383
- ? extractClawtipIdFromUrl(authUrl)
384
- : findValueAfterKeys(output, ["clawtipId", "clawtip_id", "deviceId", "device_id"]);
385
- const lower = output.toLowerCase();
386
- const failureMessage = findClawtipFailureMessage(output);
387
-
388
- return {
389
- authUrl,
390
- clawtipId: clawtipId ? decodeClawtipValue(clawtipId) : undefined,
391
- mediaPath,
392
- failureMessage,
393
- requiresWalletAuth: Boolean(authUrl || clawtipId || mediaPath
394
- || lower.includes("authurl")
395
- || lower.includes("qrcode")
396
- || lower.includes("scan")
397
- || lower.includes("扫码")),
398
- walletReady: lower.includes("register success")
399
- || lower.includes("注册成功")
400
- || lower.includes("tokeninfo")
401
- || lower.includes("config.json")
402
- || lower.includes("status: successful")
403
- || lower.includes("saved u"),
404
- };
405
- }
406
-
407
- /**
408
- * 取得 ClawTip 引导流程中要展示给用户的 QR 图片本地路径:
409
- * 优先用 `parsedOutput.mediaPath`;缺省时抛错(因为首次绑定必须先走 wallet QR 流程)。
410
- *
411
- * @param parsedOutput `parseClawtipCliOutput()` 的结果
412
- * @param orderFile 当前订单的 JSON 文件路径(用于错误消息)
413
- * @returns QR 图片绝对路径
414
- * @throws 当 `parsedOutput.mediaPath` 缺失时
415
- */
416
- export function resolveClawtipQrMediaPath(parsedOutput: ParsedClawtipOutput, orderFile: string): string {
417
- if (parsedOutput.mediaPath) {
418
- return parsedOutput.mediaPath;
419
- }
420
- throw new Error(
421
- [
422
- "ClawTip pay did not return a QR media file.",
423
- `Order file: ${orderFile}`,
424
- "For first-time wallet binding, run the ClawTip wallet QR flow before paying.",
425
- ].join(" ")
426
- );
427
- }
428
-
429
- /**
430
- * 读 order JSON 里的 `payCredential`(snake/camel 兼容)。
431
- * 用于在 `startClawtipWalletBootstrap()` 之后回传给上游 service。
432
- *
433
- * @param orderFile order JSON 的绝对路径
434
- * @returns 非空 `payCredential` 字符串;文件不存在 / 字段缺失时返回 `undefined`
435
- */
436
- export function readClawtipPayCredential(orderFile: string): string | undefined {
437
- if (!fs.existsSync(orderFile)) {
438
- return undefined;
439
- }
440
- const order = JSON.parse(fs.readFileSync(orderFile, "utf8")) as Record<string, unknown>;
441
- const credential = order.payCredential || order.pay_credential;
442
- return typeof credential === "string" && credential.trim() ? credential : undefined;
443
- }
444
-
445
- function asRecord(value: unknown): Record<string, unknown> | undefined {
446
- return value && typeof value === "object" && !Array.isArray(value)
447
- ? value as Record<string, unknown>
448
- : undefined;
449
- }
450
-
451
- function stringField(source: Record<string, unknown>, key: string): string | undefined {
452
- const value = source[key];
453
- return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
454
- }
455
-
456
- function numberField(source: Record<string, unknown>, key: string): number | undefined {
457
- const value = source[key];
458
- return typeof value === "number" && Number.isFinite(value) ? value : undefined;
459
- }
460
-
461
- function findClawtipPayment(payload: ClawtipProofProviderPayload): ClawtipBootstrapPayment | undefined {
462
- const instructions = asRecord(payload.paymentInstructions);
463
- const direct = asRecord(instructions?.clawtip);
464
- const option = Array.isArray(instructions?.options)
465
- ? instructions.options
466
- .map(asRecord)
467
- .find((entry) => entry?.method === "clawtip" && asRecord(entry.clawtip))
468
- : undefined;
469
- const clawtip = direct || asRecord(option?.clawtip);
470
- if (!clawtip) {
471
- return undefined;
472
- }
473
-
474
- const orderNo = stringField(clawtip, "orderNo") || stringField(clawtip, "order_no");
475
- const indicator = stringField(clawtip, "indicator");
476
- const amountFen = numberField(clawtip, "amountFen") ?? numberField(clawtip, "amount");
477
- if (!orderNo || !indicator || amountFen === undefined) {
478
- return undefined;
479
- }
480
-
481
- return {
482
- orderNo,
483
- indicator,
484
- amountFen,
485
- payTo: stringField(clawtip, "payTo") || stringField(clawtip, "pay_to"),
486
- encryptedData: stringField(clawtip, "encryptedData") || stringField(clawtip, "encrypted_data"),
487
- slug: stringField(clawtip, "slug"),
488
- skillId: stringField(clawtip, "skillId") || stringField(clawtip, "skill-id"),
489
- description: stringField(clawtip, "description"),
490
- resourceUrl: stringField(clawtip, "resourceUrl") || stringField(clawtip, "resource_url"),
491
- };
492
- }
493
-
494
- export async function createClawtipPaymentProof(
495
- payload: ClawtipProofProviderPayload,
496
- options: StartClawtipWalletBootstrapOptions = {},
497
- ): Promise<string> {
498
- const payment = findClawtipPayment(payload);
499
- if (!payment) {
500
- throw new Error("ClawTip proof payload is missing paymentInstructions.clawtip order data");
501
- }
502
-
503
- const result = await startClawtipWalletBootstrap(payment, options);
504
- if (!result.payCredential) {
505
- throw new Error(`ClawTip pay did not write payCredential to the order file: ${result.orderFile}`);
506
- }
507
- return result.payCredential;
508
- }
509
-
510
- /**
511
- * 把 ClawTip 引导支付所需的所有字段写成本地 order JSON:
512
- * `~/.openclaw/skills/orders/<indicator>/<orderNo>.json`。
513
- * 自动创建父目录。
514
- *
515
- * @param payment 引导支付参数
516
- * @param home 用户 home 目录(默认 `process.env.HOME` / `os.homedir()`)
517
- * @returns 写入的 order JSON 绝对路径
518
- */
519
- export function writeClawtipOrderFile(
520
- payment: ClawtipBootstrapPayment,
521
- home: string = defaultHomeDir(),
522
- ): string {
523
- const orderFile = clawtipOrderFilePath(home, payment.indicator, payment.orderNo);
524
- fs.mkdirSync(path.dirname(orderFile), { recursive: true });
525
- const orderJson = {
526
- "skill-id": payment.skillId,
527
- order_no: payment.orderNo,
528
- amount: payment.amountFen,
529
- question: "LLM inference purchase",
530
- encrypted_data: payment.encryptedData,
531
- pay_to: payment.payTo,
532
- description: payment.description,
533
- slug: payment.slug,
534
- resource_url: payment.resourceUrl,
535
- };
536
- fs.writeFileSync(orderFile, JSON.stringify(orderJson, null, 2), "utf8");
537
- return orderFile;
538
- }
539
-
540
- /**
541
- * 启动 ClawTip 钱包引导流程:
542
- * 1. 写本地 order JSON
543
- * 2. 调用 `npx @clawtip/clawtip-cli pay` 完成下单
544
- * 3. 解析输出;若无 `mediaPath` 会在 `~/.openclaw/workspace/clawtip/qrcode/` 里
545
- * 找最近生成的 QR 图作为兜底
546
- * 4. 返回 order 文件路径 + 解析结果 + 回读到的 `payCredential`
547
- *
548
- * @param payment 引导支付参数
549
- * @param options 可注入的 home / runClawtipCommand
550
- * @returns 引导结果
551
- * @throws 当 `parsedOutput.failureMessage` 非空时(即 ClawTip 下单失败)
552
- */
553
- export async function startClawtipWalletBootstrap(
554
- payment: ClawtipBootstrapPayment,
555
- options: StartClawtipWalletBootstrapOptions = {},
556
- ): Promise<{ orderFile: string; parsedOutput: ParsedClawtipOutput; payCredential?: string }> {
557
- const orderFile = writeClawtipOrderFile(payment, options.home);
558
- const home = options.home || defaultHomeDir();
559
- const payStartedAt = Date.now();
560
- const runCommand = options.runClawtipCommand || runClawtipCli;
561
- const output = await runCommand([
562
- "--yes",
563
- `@clawtip/clawtip-cli@${CLAWTIP_MIN_CLI_VERSION}`,
564
- "pay",
565
- "-o",
566
- payment.orderNo,
567
- "-i",
568
- payment.indicator,
569
- "-v",
570
- CLAWTIP_MIN_SKILL_VERSION,
571
- ]);
572
- const parsedOutput = parseClawtipCliOutput(output);
573
- if (parsedOutput.failureMessage) {
574
- throw new Error(`ClawTip pay failed: ${parsedOutput.failureMessage}`);
575
- }
576
- if (!parsedOutput.mediaPath) {
577
- parsedOutput.mediaPath = findLatestGeneratedQrMediaPath(home, payStartedAt);
578
- if (parsedOutput.mediaPath) {
579
- parsedOutput.requiresWalletAuth = true;
580
- }
581
- }
582
-
583
- return {
584
- orderFile,
585
- parsedOutput,
586
- payCredential: readClawtipPayCredential(orderFile),
587
- };
588
- }
589
-
590
- /**
591
- * 校验本机 `openclaw` CLI 可用:执行 `openclaw --version` 并返回去空白的版本字符串。
592
- *
593
- * @param options 可注入的 runOpenClawCommand
594
- * @returns OpenClaw 版本字符串
595
- * @throws 当 CLI 缺失或退出码非 0 时
596
- */
597
- export async function checkOpenClawRuntime(
598
- options: CheckOpenClawRuntimeOptions = {},
599
- ): Promise<string> {
600
- const runOpenClawCommand = options.runOpenClawCommand || runOpenClawCli;
601
- try {
602
- return (await runOpenClawCommand(["--version"])).trim();
603
- } catch (error) {
604
- const message = error instanceof Error ? error.message : String(error);
605
- throw new Error(
606
- `OpenClaw CLI is required before ClawTip wallet registration. Ensure \`openclaw --version\` works. ${message}`
607
- );
608
- }
609
- }
610
-
611
- async function defaultCheckRegister(clawtipId: string): Promise<void> {
612
- try {
613
- await runClawtipCli([
614
- "--yes",
615
- `@clawtip/clawtip-cli@${CLAWTIP_MIN_CLI_VERSION}`,
616
- "check-register",
617
- "-d",
618
- clawtipId,
619
- ]);
620
- } catch {
621
- // The Rust version treats failed check-register polls as normal pending state.
622
- }
623
- }
624
-
625
- async function sleepUntilNextPoll(
626
- ms: number,
627
- sleep: (ms: number) => Promise<void>,
628
- isCancelled: () => boolean,
629
- ): Promise<boolean> {
630
- let remaining = ms;
631
- while (remaining > 0) {
632
- if (isCancelled()) {
633
- return false;
634
- }
635
- const slice = Math.min(remaining, SLEEP_SLICE_MS);
636
- await sleep(slice);
637
- remaining -= slice;
638
- }
639
- return !isCancelled();
640
- }
641
-
642
- /**
643
- * 阻塞等待 ClawTip 钱包激活完成:轮询 `~/.openclaw/configs/config.json` 是否出现;
644
- * 拿到 `clawtipId` 时还会调 `check-register` 主动推一次。
645
- * 监听 `SIGINT` 与外部 `isCancelled` 触发取消。
646
- *
647
- * @param options 可注入的 inspect / checkRegister / cancel / sleep / pollInterval
648
- * @returns 钱包就绪返回 `true`;被取消返回 `false`
649
- */
650
- export async function waitForClawtipActivationConfirmation(
651
- options: WaitForClawtipActivationOptions = {},
652
- ): Promise<boolean> {
653
- let cancelled = false;
654
- const signalHandler = () => {
655
- cancelled = true;
656
- };
657
- process.once("SIGINT", signalHandler);
658
-
659
- const inspectWalletConfig = options.inspectWalletConfig || inspectOpenClawWalletConfig;
660
- const externalCancelled = options.isCancelled || (() => false);
661
- const isCancelled = () => cancelled || externalCancelled();
662
- const checkRegister = options.checkRegister || defaultCheckRegister;
663
- const cancel = options.cancel || p.cancel;
664
- const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
665
- const sleep = options.sleep || defaultSleep;
666
-
667
- try {
668
- for (;;) {
669
- if (isCancelled()) {
670
- cancel("ClawTip activation cancelled.");
671
- return false;
672
- }
673
-
674
- const walletConfig = inspectWalletConfig();
675
- if (walletConfig.exists) {
676
- return true;
677
- }
678
-
679
- if (options.clawtipId) {
680
- await checkRegister(options.clawtipId);
681
- if (inspectWalletConfig().exists) {
682
- return true;
683
- }
684
- }
685
-
686
- const shouldContinue = await sleepUntilNextPoll(pollIntervalMs, sleep, isCancelled);
687
- if (!shouldContinue) {
688
- cancel("ClawTip activation cancelled.");
689
- return false;
690
- }
691
- }
692
- } finally {
693
- process.removeListener("SIGINT", signalHandler);
694
- }
695
- }