@tokenbuddy/tokenbuddy 1.0.17 → 1.0.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.
@@ -10,7 +10,7 @@ import { fetchClawtipBootstrap } from "./clawtip-bootstrap.js";
10
10
  import { inspectOpenClawWalletConfig, } from "./init-payment-options.js";
11
11
  import { startClawtipWalletBootstrap, waitForClawtipActivationConfirmation, } from "./init-clawtip-activation.js";
12
12
  import { applyProviderInstall, detectProviders, previewProviderInstall, rollbackProviderInstall, } from "./provider-install.js";
13
- import { discoverSellerBackedModels, fetchSellerRegistry, normalizeSellerUrl, RegistryTooLargeError, } from "./seller-catalog.js";
13
+ import { discoverSellerBackedModels, fetchSellerRegistry, isBuyerVisibleRegistrySeller, normalizeSellerUrl, RegistryTooLargeError, } from "./seller-catalog.js";
14
14
  import { ModelIndex } from "./model-index.js";
15
15
  import { PrewarmCache, prewarmKey } from "./prewarm-cache.js";
16
16
  import { CreditTracker } from "./credit-tracker.js";
@@ -19,6 +19,7 @@ import { RouteFailover } from "./route-failover.js";
19
19
  import { PrewarmScheduler } from "./prewarm-scheduler.js";
20
20
  import { planSellerRouteSet } from "./seller-route-planner.js";
21
21
  import { assertSellerRoutingConfig, mergeSellerRoutingConfig, normalizeSellerRoutingConfig, parseSellerIdList, ROUTING_CONFIG_KEY } from "./seller-routing-config.js";
22
+ import { assertInitSetupSteps, buildCompletedInitSetupMarker, INIT_SETUP_CONFIG_KEY, INIT_SETUP_STEPS, isFreshInitMachine, normalizeInitSetupMarker, resolveInitRecommendedModels, } from "./init-setup.js";
22
23
  const logger = createModuleLogger("tb-proxyd");
23
24
  const FOCUS_SET_CONFIG_KEY = "focus-set";
24
25
  const PROXY_JSON_BODY_LIMIT = "10mb";
@@ -623,6 +624,218 @@ export class TokenbuddyDaemon {
623
624
  store: this.tokenStore.summary()
624
625
  };
625
626
  }
627
+ livePayments() {
628
+ return this.tokenStore.listPayments().map((payment) => withLiveClawtipWalletState(payment, this.config.clawtipHomeDir));
629
+ }
630
+ clientToolsSummary() {
631
+ const providerStatuses = detectProviders({ home: this.config.providerHomeDir }).map(clientToolStatusFromProvider);
632
+ const clients = [
633
+ ...providerStatuses,
634
+ buildCustomClientToolStatus(this.activeProxyPort()),
635
+ ];
636
+ const configuredCount = clients.filter((client) => client.configured).length;
637
+ const detectedCount = clients.filter((client) => client.detected && client.status !== "manual").length;
638
+ return {
639
+ clients,
640
+ summary: {
641
+ configuredCount,
642
+ detectedCount,
643
+ totalCount: clients.length,
644
+ installCommand: "tb init"
645
+ }
646
+ };
647
+ }
648
+ initRepairStatus(input) {
649
+ if (input.setup.status !== "completed") {
650
+ return { repairMode: false, repairReasons: [] };
651
+ }
652
+ const completedSteps = new Set(input.setup.completedSteps);
653
+ const missingSteps = INIT_SETUP_STEPS.filter((step) => !completedSteps.has(step));
654
+ const repairReasons = [];
655
+ if (missingSteps.length > 0) {
656
+ repairReasons.push(`missing_steps:${missingSteps.join(",")}`);
657
+ }
658
+ if (input.focusSet.length === 0) {
659
+ repairReasons.push("missing_focus_models");
660
+ }
661
+ if (!input.payments.some((payment) => payment.enabled)) {
662
+ repairReasons.push("missing_payment_method");
663
+ }
664
+ if (input.clientsSummary.configuredCount === 0) {
665
+ repairReasons.push("missing_connected_tools");
666
+ }
667
+ return {
668
+ repairMode: repairReasons.length > 0,
669
+ repairReasons,
670
+ };
671
+ }
672
+ initStateSnapshot() {
673
+ const record = this.tokenStore.getDaemonRuntimeConfig(INIT_SETUP_CONFIG_KEY);
674
+ const setup = normalizeInitSetupMarker(record?.config);
675
+ const storedRouting = this.tokenStore.getDaemonRuntimeConfig(ROUTING_CONFIG_KEY)?.config;
676
+ const routingSource = storedRouting !== undefined ? "store" : this.config.sellerRouting ? "config" : "default";
677
+ const clients = this.clientToolsSummary();
678
+ const payments = this.livePayments();
679
+ const focusSet = this.resolveFocusSet();
680
+ const recommendedModels = resolveInitRecommendedModels({
681
+ configuredModels: this.config.initRecommendedModels,
682
+ env: process.env,
683
+ });
684
+ const repair = this.initRepairStatus({
685
+ setup,
686
+ payments,
687
+ clientsSummary: clients.summary,
688
+ focusSet,
689
+ });
690
+ return {
691
+ setup: {
692
+ ...setup,
693
+ createdAt: record?.createdAt,
694
+ updatedAt: record?.updatedAt ?? setup.updatedAt,
695
+ },
696
+ freshMachine: isFreshInitMachine(setup),
697
+ repairMode: repair.repairMode,
698
+ repairReasons: repair.repairReasons,
699
+ runtime: this.runtimeSummary(),
700
+ payments,
701
+ clients: clients.clients,
702
+ clientsSummary: clients.summary,
703
+ routing: {
704
+ strategy: this.sellerRouting,
705
+ source: routingSource,
706
+ },
707
+ focusSet,
708
+ recommendedModels,
709
+ };
710
+ }
711
+ async buildInitDoctorReport() {
712
+ const catalog = await this.initDoctorCatalogSnapshot();
713
+ const currentRouting = this.refreshSellerRoutingConfig();
714
+ const payments = this.livePayments().filter((payment) => payment.enabled);
715
+ const clients = this.clientToolsSummary();
716
+ const routeModelId = this.resolveFocusSet()[0] || catalog.models[0]?.id;
717
+ const routingPreview = routeModelId ? this.buildRoutingPreview({ modelId: routeModelId, routing: currentRouting }) : undefined;
718
+ const checks = [
719
+ {
720
+ id: "local_service",
721
+ label: "本地服务",
722
+ status: this.controlServer && this.proxyServer ? "passed" : "failed",
723
+ message: this.controlServer && this.proxyServer
724
+ ? "tb-proxyd 正在运行,控制面和代理端口已经打开。"
725
+ : "tb-proxyd 本地服务尚未完全启动。",
726
+ details: [
727
+ `控制面:http://127.0.0.1:${this.activeControlPort()}`,
728
+ `代理:http://127.0.0.1:${this.activeProxyPort()}`
729
+ ]
730
+ },
731
+ {
732
+ id: "proxy_interface",
733
+ label: "代理接口",
734
+ status: this.proxyServer ? "passed" : "failed",
735
+ message: this.proxyServer
736
+ ? "OpenAI 和 Anthropic 兼容本地接口已就绪。"
737
+ : "本地代理接口尚未就绪。",
738
+ details: [
739
+ `OpenAI 兼容 Base URL:http://127.0.0.1:${this.activeProxyPort()}/v1`,
740
+ `Anthropic 兼容 Base URL:http://127.0.0.1:${this.activeProxyPort()}`
741
+ ]
742
+ },
743
+ {
744
+ id: "seller_registry",
745
+ label: "供应商注册表",
746
+ status: catalog.available && catalog.sellers.length > 0 ? "passed" : "failed",
747
+ message: catalog.available && catalog.sellers.length > 0
748
+ ? `已加载 ${catalog.sellers.length} 个供应商。`
749
+ : "TokenBuddy 还没有可用供应商注册表。",
750
+ details: [
751
+ `来源:${formatInitDoctorCatalogSource(catalog.source)}`,
752
+ ...(catalog.errorMessage ? [catalog.errorMessage] : [])
753
+ ]
754
+ },
755
+ {
756
+ id: "model_catalog",
757
+ label: "模型目录",
758
+ status: catalog.available && catalog.models.length > 0 ? "passed" : "failed",
759
+ message: catalog.available && catalog.models.length > 0
760
+ ? `已发现 ${new Set(catalog.models.map((model) => model.id)).size} 个可用模型。`
761
+ : "TokenBuddy 还没有加载到供应商支持的模型。",
762
+ details: catalog.models.slice(0, 5).map((model) => model.id)
763
+ },
764
+ {
765
+ id: "routing_strategy",
766
+ label: "路由策略",
767
+ status: routingPreview && !("error" in routingPreview.plan) && routingPreview.plan.routes.length > 0 ? "passed" : "failed",
768
+ message: routingPreview && !("error" in routingPreview.plan) && routingPreview.plan.routes.length > 0
769
+ ? `当前策略可以为 ${routingPreview.modelId} 找到 ${routingPreview.plan.routes.length} 条供应商路径。`
770
+ : "当前模型和路由策略下没有可用供应商。",
771
+ details: routingPreview
772
+ ? ["error" in routingPreview.plan
773
+ ? `原因:${routingPreview.plan.error}`
774
+ : `策略:${routingPreview.plan.mode}:${routingPreview.plan.scorer}`]
775
+ : ["没有可用于路由验证的模型。"]
776
+ },
777
+ {
778
+ id: "payment_method",
779
+ label: "支付方式",
780
+ status: payments.length > 0 ? "passed" : "warning",
781
+ message: payments.length > 0
782
+ ? `已启用 ${payments.length} 个支付方式。`
783
+ : "尚未启用支付方式;自动购买会在绑定支付前暂停。",
784
+ details: payments.map((payment) => payment.method)
785
+ },
786
+ {
787
+ id: "connected_tools",
788
+ label: "已连接工具",
789
+ status: clients.summary.configuredCount > 0 ? "passed" : "warning",
790
+ message: clients.summary.configuredCount > 0
791
+ ? `已配置 ${clients.summary.configuredCount} 个 AI 工具。`
792
+ : "尚未检测到已接入 TokenBuddy 的 AI 工具。",
793
+ details: clients.clients
794
+ .filter((client) => client.configured)
795
+ .map((client) => client.name)
796
+ }
797
+ ];
798
+ const status = checks.some((check) => check.status === "failed")
799
+ ? "failed"
800
+ : checks.some((check) => check.status === "warning" || check.status === "skipped")
801
+ ? "warning"
802
+ : "passed";
803
+ return {
804
+ status,
805
+ generatedAt: new Date().toISOString(),
806
+ checks
807
+ };
808
+ }
809
+ async initDoctorCatalogSnapshot() {
810
+ try {
811
+ const catalog = await this.listSellerBackedModels();
812
+ return {
813
+ available: true,
814
+ models: catalog.models,
815
+ sellers: catalog.sellers,
816
+ source: "live"
817
+ };
818
+ }
819
+ catch (error) {
820
+ const errorMessage = error instanceof Error ? error.message : String(error);
821
+ if (this.lastRegistrySnapshot) {
822
+ const cached = catalogSnapshotFromRegistry(this.lastRegistrySnapshot);
823
+ return {
824
+ ...cached,
825
+ available: cached.sellers.length > 0,
826
+ source: "cached",
827
+ errorMessage
828
+ };
829
+ }
830
+ return {
831
+ available: false,
832
+ models: [],
833
+ sellers: [],
834
+ source: "unavailable",
835
+ errorMessage
836
+ };
837
+ }
838
+ }
626
839
  endpointProtocol(endpoint) {
627
840
  if (endpoint === "/v1/chat/completions") {
628
841
  return "chat_completions";
@@ -986,6 +1199,7 @@ export class TokenbuddyDaemon {
986
1199
  }
987
1200
  async listSellerBackedModels() {
988
1201
  const catalog = await discoverSellerBackedModels(this.config.sellerRegistryUrl);
1202
+ this.lastRegistrySnapshot = sellerCatalogResultToRegistrySnapshot(catalog);
989
1203
  return {
990
1204
  models: catalog.models,
991
1205
  sellers: catalog.sellers
@@ -1943,9 +2157,74 @@ export class TokenbuddyDaemon {
1943
2157
  controlApp.get("/payments", (req, res) => {
1944
2158
  logger.info("control.payments.requested", "control payments requested", {});
1945
2159
  res.status(200).json({
1946
- payments: this.tokenStore.listPayments().map((payment) => withLiveClawtipWalletState(payment, this.config.clawtipHomeDir))
2160
+ payments: this.livePayments()
1947
2161
  });
1948
2162
  });
2163
+ controlApp.get("/init/state", (req, res) => {
2164
+ try {
2165
+ const state = this.initStateSnapshot();
2166
+ logger.info("init.state.requested", "init setup state requested", {
2167
+ setupStatus: state.setup.status,
2168
+ freshMachine: state.freshMachine,
2169
+ configuredClientCount: state.clientsSummary.configuredCount,
2170
+ paymentCount: state.payments.length
2171
+ });
2172
+ res.status(200).json(state);
2173
+ }
2174
+ catch (error) {
2175
+ const errorMessage = error instanceof Error ? error.message : String(error);
2176
+ logger.warn("init.state.failed", "init setup state failed", { errorMessage });
2177
+ res.status(500).json({
2178
+ error: {
2179
+ code: "init_state_failed",
2180
+ message: errorMessage
2181
+ }
2182
+ });
2183
+ }
2184
+ });
2185
+ controlApp.post("/init/complete", (req, res) => {
2186
+ try {
2187
+ const completedSteps = assertInitSetupSteps(req.body?.completedSteps);
2188
+ const setup = buildCompletedInitSetupMarker(completedSteps);
2189
+ this.tokenStore.saveDaemonRuntimeConfig(INIT_SETUP_CONFIG_KEY, setup);
2190
+ const state = this.initStateSnapshot();
2191
+ logger.info("init.setup.completed", "init setup marker completed", {
2192
+ completedStepCount: setup.completedSteps.length
2193
+ });
2194
+ res.status(200).json(state);
2195
+ }
2196
+ catch (error) {
2197
+ const errorMessage = error instanceof Error ? error.message : String(error);
2198
+ logger.warn("init.setup.complete_failed", "init setup complete failed", { errorMessage });
2199
+ res.status(400).json({
2200
+ error: {
2201
+ code: "init_setup_complete_failed",
2202
+ message: errorMessage
2203
+ }
2204
+ });
2205
+ }
2206
+ });
2207
+ controlApp.post("/init/doctor/run", async (_req, res) => {
2208
+ try {
2209
+ const report = await this.buildInitDoctorReport();
2210
+ logger.info("init.doctor.completed", "init doctor completed", {
2211
+ status: report.status,
2212
+ failedCount: report.checks.filter((check) => check.status === "failed").length,
2213
+ warningCount: report.checks.filter((check) => check.status === "warning").length
2214
+ });
2215
+ res.status(200).json(report);
2216
+ }
2217
+ catch (error) {
2218
+ const errorMessage = error instanceof Error ? error.message : String(error);
2219
+ logger.warn("init.doctor.failed", "init doctor failed", { errorMessage });
2220
+ res.status(500).json({
2221
+ error: {
2222
+ code: "init_doctor_failed",
2223
+ message: errorMessage
2224
+ }
2225
+ });
2226
+ }
2227
+ });
1949
2228
  controlApp.post("/payments/clawtip/activate", async (req, res) => {
1950
2229
  try {
1951
2230
  const qr = await this.startClawtipActivationQr();
@@ -2118,27 +2397,13 @@ export class TokenbuddyDaemon {
2118
2397
  });
2119
2398
  controlApp.get("/providers/status", (_req, res) => {
2120
2399
  try {
2121
- const providerStatuses = detectProviders({ home: this.config.providerHomeDir }).map(clientToolStatusFromProvider);
2122
- const clients = [
2123
- ...providerStatuses,
2124
- buildCustomClientToolStatus(this.activeProxyPort()),
2125
- ];
2126
- const configuredCount = clients.filter((client) => client.configured).length;
2127
- const detectedCount = clients.filter((client) => client.detected && client.status !== "manual").length;
2400
+ const status = this.clientToolsSummary();
2128
2401
  logger.info("provider.status.requested", "provider status requested", {
2129
- clientCount: clients.length,
2130
- configuredCount,
2131
- detectedCount
2132
- });
2133
- res.status(200).json({
2134
- clients,
2135
- summary: {
2136
- configuredCount,
2137
- detectedCount,
2138
- totalCount: clients.length,
2139
- installCommand: "tb init"
2140
- }
2402
+ clientCount: status.clients.length,
2403
+ configuredCount: status.summary.configuredCount,
2404
+ detectedCount: status.summary.detectedCount
2141
2405
  });
2406
+ res.status(200).json(status);
2142
2407
  }
2143
2408
  catch (error) {
2144
2409
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -2670,6 +2935,55 @@ function routingKey(routing) {
2670
2935
  ...fixedByModel
2671
2936
  ].join("\u0001");
2672
2937
  }
2938
+ function formatInitDoctorCatalogSource(source) {
2939
+ if (source === "live") {
2940
+ return "实时注册表";
2941
+ }
2942
+ if (source === "cached") {
2943
+ return "本机缓存";
2944
+ }
2945
+ return "不可用";
2946
+ }
2947
+ function sellerCatalogResultToRegistrySnapshot(catalog) {
2948
+ return {
2949
+ version: catalog.version,
2950
+ defaultSeller: catalog.defaultSeller,
2951
+ sellers: catalog.sellers
2952
+ .filter((seller) => seller.status === "ok")
2953
+ .map((seller) => ({
2954
+ id: seller.id,
2955
+ name: seller.name,
2956
+ status: "active",
2957
+ url: seller.url,
2958
+ supportedProtocols: seller.supportedProtocols,
2959
+ paymentMethods: seller.paymentMethods,
2960
+ models: catalog.models
2961
+ .filter((model) => model.sellerId === seller.id)
2962
+ .map((model) => model.id)
2963
+ }))
2964
+ };
2965
+ }
2966
+ function catalogSnapshotFromRegistry(registry) {
2967
+ const visibleSellers = registry.sellers.filter(isBuyerVisibleRegistrySeller);
2968
+ return {
2969
+ available: visibleSellers.length > 0,
2970
+ source: "cached",
2971
+ models: visibleSellers.flatMap((seller) => (seller.models ?? []).map((modelId) => ({
2972
+ id: modelId,
2973
+ sellerId: seller.id,
2974
+ sellerName: seller.name,
2975
+ sellerUrl: seller.url,
2976
+ supportedProtocols: seller.supportedProtocols ?? [],
2977
+ paymentMethods: seller.paymentMethods ?? []
2978
+ }))),
2979
+ sellers: visibleSellers.map((seller) => ({
2980
+ id: seller.id,
2981
+ name: seller.name,
2982
+ url: seller.url,
2983
+ status: seller.status ?? "active"
2984
+ }))
2985
+ };
2986
+ }
2673
2987
  /**
2674
2988
  * 从 query string 构造 `BuyerSellerRoutingConfig` override(用于 GET /routing/preview)。
2675
2989
  * 接受 `mode` / `scorer` / `sellerId` / `sellerIds`(逗号分隔)。