@tokenbuddy/tokenbuddy 1.0.16 → 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.
- package/bin/tb-proxyd.js +2 -0
- package/bin/tb.js +4 -0
- package/dist/src/cli.d.ts +52 -1
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +178 -8
- package/dist/src/cli.js.map +1 -1
- package/dist/src/daemon.d.ts +8 -0
- package/dist/src/daemon.d.ts.map +1 -1
- package/dist/src/daemon.js +335 -21
- package/dist/src/daemon.js.map +1 -1
- package/dist/src/init-setup.d.ts +25 -0
- package/dist/src/init-setup.d.ts.map +1 -0
- package/dist/src/init-setup.js +125 -0
- package/dist/src/init-setup.js.map +1 -0
- package/package.json +1 -1
- package/src/cli.ts +233 -10
- package/src/daemon.ts +385 -20
- package/src/init-setup.ts +165 -0
- package/static/ui/assets/index-BILQcD8g.js +226 -0
- package/static/ui/assets/index-BILQcD8g.js.map +1 -0
- package/static/ui/assets/index-C5KsebCA.css +1 -0
- package/static/ui/index.html +2 -2
- package/static/ui/manifest.webmanifest +1 -1
- package/static/ui/sw.js +1 -1
- package/tests/control-plane-ui-endpoints.test.ts +193 -1
- package/tests/tokenbuddy.test.ts +209 -0
- package/static/ui/assets/index-UMiTTeo8.css +0 -1
- package/static/ui/assets/index-YHs-Ca0f.js +0 -206
- package/static/ui/assets/index-YHs-Ca0f.js.map +0 -1
package/src/daemon.ts
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
manifestModelIds,
|
|
34
34
|
manifestPaymentMethods,
|
|
35
35
|
manifestProtocols,
|
|
36
|
+
isBuyerVisibleRegistrySeller,
|
|
36
37
|
normalizeSellerUrl,
|
|
37
38
|
RegistryTooLargeError,
|
|
38
39
|
type RegistrySeller,
|
|
@@ -56,6 +57,16 @@ import {
|
|
|
56
57
|
ROUTING_CONFIG_KEY,
|
|
57
58
|
type BuyerSellerRoutingConfig
|
|
58
59
|
} from "./seller-routing-config.js";
|
|
60
|
+
import {
|
|
61
|
+
assertInitSetupSteps,
|
|
62
|
+
buildCompletedInitSetupMarker,
|
|
63
|
+
INIT_SETUP_CONFIG_KEY,
|
|
64
|
+
INIT_SETUP_STEPS,
|
|
65
|
+
type InitSetupMarker,
|
|
66
|
+
isFreshInitMachine,
|
|
67
|
+
normalizeInitSetupMarker,
|
|
68
|
+
resolveInitRecommendedModels,
|
|
69
|
+
} from "./init-setup.js";
|
|
59
70
|
|
|
60
71
|
const logger = createModuleLogger("tb-proxyd");
|
|
61
72
|
const FOCUS_SET_CONFIG_KEY = "focus-set";
|
|
@@ -99,6 +110,31 @@ interface ClientToolStatus {
|
|
|
99
110
|
};
|
|
100
111
|
}
|
|
101
112
|
|
|
113
|
+
type InitDoctorCheckStatus = "passed" | "warning" | "failed" | "skipped";
|
|
114
|
+
type InitDoctorStatus = "passed" | "warning" | "failed";
|
|
115
|
+
|
|
116
|
+
interface InitDoctorCheck {
|
|
117
|
+
id: string;
|
|
118
|
+
label: string;
|
|
119
|
+
status: InitDoctorCheckStatus;
|
|
120
|
+
message: string;
|
|
121
|
+
details: string[];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
interface InitDoctorReport {
|
|
125
|
+
status: InitDoctorStatus;
|
|
126
|
+
generatedAt: string;
|
|
127
|
+
checks: InitDoctorCheck[];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
interface InitDoctorCatalogSnapshot {
|
|
131
|
+
available: boolean;
|
|
132
|
+
models: Array<{ id: string; sellerId: string; sellerName?: string; sellerUrl: string; supportedProtocols: string[]; paymentMethods: string[] }>;
|
|
133
|
+
sellers: Array<{ id: string; name?: string; url: string; status: string; manifestSellerId?: string; errorMessage?: string }>;
|
|
134
|
+
source: "live" | "cached" | "unavailable";
|
|
135
|
+
errorMessage?: string;
|
|
136
|
+
}
|
|
137
|
+
|
|
102
138
|
function clientToolStatusFromProvider(provider: ProviderCandidate): ClientToolStatus {
|
|
103
139
|
return {
|
|
104
140
|
id: provider.id,
|
|
@@ -192,6 +228,8 @@ export interface DaemonConfig {
|
|
|
192
228
|
* 缺省时由 BuyerStore 历史模型使用情况 + `TB_BUYER_WARMUP_MODELS` env 推导。
|
|
193
229
|
*/
|
|
194
230
|
warmupModels?: string[];
|
|
231
|
+
/** Web init wizard recommended model ids. Env override: TB_PROXYD_INIT_RECOMMENDED_MODELS. */
|
|
232
|
+
initRecommendedModels?: string[];
|
|
195
233
|
/** 预热模型目录刷新间隔(秒) */
|
|
196
234
|
warmupRefreshIntervalSecs?: number;
|
|
197
235
|
/** 预热探测超时(毫秒) */
|
|
@@ -843,6 +881,229 @@ export class TokenbuddyDaemon {
|
|
|
843
881
|
};
|
|
844
882
|
}
|
|
845
883
|
|
|
884
|
+
private livePayments(): PaymentConfig[] {
|
|
885
|
+
return this.tokenStore.listPayments().map((payment) => withLiveClawtipWalletState(payment, this.config.clawtipHomeDir));
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
private clientToolsSummary(): { clients: ClientToolStatus[]; summary: { configuredCount: number; detectedCount: number; totalCount: number; installCommand: string } } {
|
|
889
|
+
const providerStatuses = detectProviders({ home: this.config.providerHomeDir }).map(clientToolStatusFromProvider);
|
|
890
|
+
const clients = [
|
|
891
|
+
...providerStatuses,
|
|
892
|
+
buildCustomClientToolStatus(this.activeProxyPort()),
|
|
893
|
+
];
|
|
894
|
+
const configuredCount = clients.filter((client) => client.configured).length;
|
|
895
|
+
const detectedCount = clients.filter((client) => client.detected && client.status !== "manual").length;
|
|
896
|
+
return {
|
|
897
|
+
clients,
|
|
898
|
+
summary: {
|
|
899
|
+
configuredCount,
|
|
900
|
+
detectedCount,
|
|
901
|
+
totalCount: clients.length,
|
|
902
|
+
installCommand: "tb init"
|
|
903
|
+
}
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
private initRepairStatus(input: {
|
|
908
|
+
setup: InitSetupMarker;
|
|
909
|
+
payments: PaymentConfig[];
|
|
910
|
+
clientsSummary: { configuredCount: number };
|
|
911
|
+
focusSet: string[];
|
|
912
|
+
}): { repairMode: boolean; repairReasons: string[] } {
|
|
913
|
+
if (input.setup.status !== "completed") {
|
|
914
|
+
return { repairMode: false, repairReasons: [] };
|
|
915
|
+
}
|
|
916
|
+
const completedSteps = new Set(input.setup.completedSteps);
|
|
917
|
+
const missingSteps = INIT_SETUP_STEPS.filter((step) => !completedSteps.has(step));
|
|
918
|
+
const repairReasons: string[] = [];
|
|
919
|
+
if (missingSteps.length > 0) {
|
|
920
|
+
repairReasons.push(`missing_steps:${missingSteps.join(",")}`);
|
|
921
|
+
}
|
|
922
|
+
if (input.focusSet.length === 0) {
|
|
923
|
+
repairReasons.push("missing_focus_models");
|
|
924
|
+
}
|
|
925
|
+
if (!input.payments.some((payment) => payment.enabled)) {
|
|
926
|
+
repairReasons.push("missing_payment_method");
|
|
927
|
+
}
|
|
928
|
+
if (input.clientsSummary.configuredCount === 0) {
|
|
929
|
+
repairReasons.push("missing_connected_tools");
|
|
930
|
+
}
|
|
931
|
+
return {
|
|
932
|
+
repairMode: repairReasons.length > 0,
|
|
933
|
+
repairReasons,
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
private initStateSnapshot() {
|
|
938
|
+
const record = this.tokenStore.getDaemonRuntimeConfig<unknown>(INIT_SETUP_CONFIG_KEY);
|
|
939
|
+
const setup = normalizeInitSetupMarker(record?.config);
|
|
940
|
+
const storedRouting = this.tokenStore.getDaemonRuntimeConfig<unknown>(ROUTING_CONFIG_KEY)?.config;
|
|
941
|
+
const routingSource: "store" | "config" | "default" =
|
|
942
|
+
storedRouting !== undefined ? "store" : this.config.sellerRouting ? "config" : "default";
|
|
943
|
+
const clients = this.clientToolsSummary();
|
|
944
|
+
const payments = this.livePayments();
|
|
945
|
+
const focusSet = this.resolveFocusSet();
|
|
946
|
+
const recommendedModels = resolveInitRecommendedModels({
|
|
947
|
+
configuredModels: this.config.initRecommendedModels,
|
|
948
|
+
env: process.env,
|
|
949
|
+
});
|
|
950
|
+
const repair = this.initRepairStatus({
|
|
951
|
+
setup,
|
|
952
|
+
payments,
|
|
953
|
+
clientsSummary: clients.summary,
|
|
954
|
+
focusSet,
|
|
955
|
+
});
|
|
956
|
+
return {
|
|
957
|
+
setup: {
|
|
958
|
+
...setup,
|
|
959
|
+
createdAt: record?.createdAt,
|
|
960
|
+
updatedAt: record?.updatedAt ?? setup.updatedAt,
|
|
961
|
+
},
|
|
962
|
+
freshMachine: isFreshInitMachine(setup),
|
|
963
|
+
repairMode: repair.repairMode,
|
|
964
|
+
repairReasons: repair.repairReasons,
|
|
965
|
+
runtime: this.runtimeSummary(),
|
|
966
|
+
payments,
|
|
967
|
+
clients: clients.clients,
|
|
968
|
+
clientsSummary: clients.summary,
|
|
969
|
+
routing: {
|
|
970
|
+
strategy: this.sellerRouting,
|
|
971
|
+
source: routingSource,
|
|
972
|
+
},
|
|
973
|
+
focusSet,
|
|
974
|
+
recommendedModels,
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
private async buildInitDoctorReport(): Promise<InitDoctorReport> {
|
|
979
|
+
const catalog = await this.initDoctorCatalogSnapshot();
|
|
980
|
+
const currentRouting = this.refreshSellerRoutingConfig();
|
|
981
|
+
const payments = this.livePayments().filter((payment) => payment.enabled);
|
|
982
|
+
const clients = this.clientToolsSummary();
|
|
983
|
+
const routeModelId = this.resolveFocusSet()[0] || catalog.models[0]?.id;
|
|
984
|
+
const routingPreview = routeModelId ? this.buildRoutingPreview({ modelId: routeModelId, routing: currentRouting }) : undefined;
|
|
985
|
+
const checks: InitDoctorCheck[] = [
|
|
986
|
+
{
|
|
987
|
+
id: "local_service",
|
|
988
|
+
label: "本地服务",
|
|
989
|
+
status: this.controlServer && this.proxyServer ? "passed" : "failed",
|
|
990
|
+
message: this.controlServer && this.proxyServer
|
|
991
|
+
? "tb-proxyd 正在运行,控制面和代理端口已经打开。"
|
|
992
|
+
: "tb-proxyd 本地服务尚未完全启动。",
|
|
993
|
+
details: [
|
|
994
|
+
`控制面:http://127.0.0.1:${this.activeControlPort()}`,
|
|
995
|
+
`代理:http://127.0.0.1:${this.activeProxyPort()}`
|
|
996
|
+
]
|
|
997
|
+
},
|
|
998
|
+
{
|
|
999
|
+
id: "proxy_interface",
|
|
1000
|
+
label: "代理接口",
|
|
1001
|
+
status: this.proxyServer ? "passed" : "failed",
|
|
1002
|
+
message: this.proxyServer
|
|
1003
|
+
? "OpenAI 和 Anthropic 兼容本地接口已就绪。"
|
|
1004
|
+
: "本地代理接口尚未就绪。",
|
|
1005
|
+
details: [
|
|
1006
|
+
`OpenAI 兼容 Base URL:http://127.0.0.1:${this.activeProxyPort()}/v1`,
|
|
1007
|
+
`Anthropic 兼容 Base URL:http://127.0.0.1:${this.activeProxyPort()}`
|
|
1008
|
+
]
|
|
1009
|
+
},
|
|
1010
|
+
{
|
|
1011
|
+
id: "seller_registry",
|
|
1012
|
+
label: "供应商注册表",
|
|
1013
|
+
status: catalog.available && catalog.sellers.length > 0 ? "passed" : "failed",
|
|
1014
|
+
message: catalog.available && catalog.sellers.length > 0
|
|
1015
|
+
? `已加载 ${catalog.sellers.length} 个供应商。`
|
|
1016
|
+
: "TokenBuddy 还没有可用供应商注册表。",
|
|
1017
|
+
details: [
|
|
1018
|
+
`来源:${formatInitDoctorCatalogSource(catalog.source)}`,
|
|
1019
|
+
...(catalog.errorMessage ? [catalog.errorMessage] : [])
|
|
1020
|
+
]
|
|
1021
|
+
},
|
|
1022
|
+
{
|
|
1023
|
+
id: "model_catalog",
|
|
1024
|
+
label: "模型目录",
|
|
1025
|
+
status: catalog.available && catalog.models.length > 0 ? "passed" : "failed",
|
|
1026
|
+
message: catalog.available && catalog.models.length > 0
|
|
1027
|
+
? `已发现 ${new Set(catalog.models.map((model) => model.id)).size} 个可用模型。`
|
|
1028
|
+
: "TokenBuddy 还没有加载到供应商支持的模型。",
|
|
1029
|
+
details: catalog.models.slice(0, 5).map((model) => model.id)
|
|
1030
|
+
},
|
|
1031
|
+
{
|
|
1032
|
+
id: "routing_strategy",
|
|
1033
|
+
label: "路由策略",
|
|
1034
|
+
status: routingPreview && !("error" in routingPreview.plan) && routingPreview.plan.routes.length > 0 ? "passed" : "failed",
|
|
1035
|
+
message: routingPreview && !("error" in routingPreview.plan) && routingPreview.plan.routes.length > 0
|
|
1036
|
+
? `当前策略可以为 ${routingPreview.modelId} 找到 ${routingPreview.plan.routes.length} 条供应商路径。`
|
|
1037
|
+
: "当前模型和路由策略下没有可用供应商。",
|
|
1038
|
+
details: routingPreview
|
|
1039
|
+
? ["error" in routingPreview.plan
|
|
1040
|
+
? `原因:${routingPreview.plan.error}`
|
|
1041
|
+
: `策略:${routingPreview.plan.mode}:${routingPreview.plan.scorer}`]
|
|
1042
|
+
: ["没有可用于路由验证的模型。"]
|
|
1043
|
+
},
|
|
1044
|
+
{
|
|
1045
|
+
id: "payment_method",
|
|
1046
|
+
label: "支付方式",
|
|
1047
|
+
status: payments.length > 0 ? "passed" : "warning",
|
|
1048
|
+
message: payments.length > 0
|
|
1049
|
+
? `已启用 ${payments.length} 个支付方式。`
|
|
1050
|
+
: "尚未启用支付方式;自动购买会在绑定支付前暂停。",
|
|
1051
|
+
details: payments.map((payment) => payment.method)
|
|
1052
|
+
},
|
|
1053
|
+
{
|
|
1054
|
+
id: "connected_tools",
|
|
1055
|
+
label: "已连接工具",
|
|
1056
|
+
status: clients.summary.configuredCount > 0 ? "passed" : "warning",
|
|
1057
|
+
message: clients.summary.configuredCount > 0
|
|
1058
|
+
? `已配置 ${clients.summary.configuredCount} 个 AI 工具。`
|
|
1059
|
+
: "尚未检测到已接入 TokenBuddy 的 AI 工具。",
|
|
1060
|
+
details: clients.clients
|
|
1061
|
+
.filter((client) => client.configured)
|
|
1062
|
+
.map((client) => client.name)
|
|
1063
|
+
}
|
|
1064
|
+
];
|
|
1065
|
+
const status: InitDoctorStatus = checks.some((check) => check.status === "failed")
|
|
1066
|
+
? "failed"
|
|
1067
|
+
: checks.some((check) => check.status === "warning" || check.status === "skipped")
|
|
1068
|
+
? "warning"
|
|
1069
|
+
: "passed";
|
|
1070
|
+
return {
|
|
1071
|
+
status,
|
|
1072
|
+
generatedAt: new Date().toISOString(),
|
|
1073
|
+
checks
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
private async initDoctorCatalogSnapshot(): Promise<InitDoctorCatalogSnapshot> {
|
|
1078
|
+
try {
|
|
1079
|
+
const catalog = await this.listSellerBackedModels();
|
|
1080
|
+
return {
|
|
1081
|
+
available: true,
|
|
1082
|
+
models: catalog.models,
|
|
1083
|
+
sellers: catalog.sellers,
|
|
1084
|
+
source: "live"
|
|
1085
|
+
};
|
|
1086
|
+
} catch (error: unknown) {
|
|
1087
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1088
|
+
if (this.lastRegistrySnapshot) {
|
|
1089
|
+
const cached = catalogSnapshotFromRegistry(this.lastRegistrySnapshot);
|
|
1090
|
+
return {
|
|
1091
|
+
...cached,
|
|
1092
|
+
available: cached.sellers.length > 0,
|
|
1093
|
+
source: "cached",
|
|
1094
|
+
errorMessage
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
return {
|
|
1098
|
+
available: false,
|
|
1099
|
+
models: [],
|
|
1100
|
+
sellers: [],
|
|
1101
|
+
source: "unavailable",
|
|
1102
|
+
errorMessage
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
846
1107
|
private endpointProtocol(endpoint: string): string | undefined {
|
|
847
1108
|
if (endpoint === "/v1/chat/completions") {
|
|
848
1109
|
return "chat_completions";
|
|
@@ -1273,6 +1534,7 @@ export class TokenbuddyDaemon {
|
|
|
1273
1534
|
sellers: Array<{ id: string; name?: string; url: string; status: string; manifestSellerId?: string; errorMessage?: string }>;
|
|
1274
1535
|
}> {
|
|
1275
1536
|
const catalog = await discoverSellerBackedModels(this.config.sellerRegistryUrl);
|
|
1537
|
+
this.lastRegistrySnapshot = sellerCatalogResultToRegistrySnapshot(catalog);
|
|
1276
1538
|
return {
|
|
1277
1539
|
models: catalog.models,
|
|
1278
1540
|
sellers: catalog.sellers
|
|
@@ -2320,10 +2582,75 @@ export class TokenbuddyDaemon {
|
|
|
2320
2582
|
controlApp.get("/payments", (req, res) => {
|
|
2321
2583
|
logger.info("control.payments.requested", "control payments requested", {});
|
|
2322
2584
|
res.status(200).json({
|
|
2323
|
-
payments: this.
|
|
2585
|
+
payments: this.livePayments()
|
|
2324
2586
|
});
|
|
2325
2587
|
});
|
|
2326
2588
|
|
|
2589
|
+
controlApp.get("/init/state", (req, res) => {
|
|
2590
|
+
try {
|
|
2591
|
+
const state = this.initStateSnapshot();
|
|
2592
|
+
logger.info("init.state.requested", "init setup state requested", {
|
|
2593
|
+
setupStatus: state.setup.status,
|
|
2594
|
+
freshMachine: state.freshMachine,
|
|
2595
|
+
configuredClientCount: state.clientsSummary.configuredCount,
|
|
2596
|
+
paymentCount: state.payments.length
|
|
2597
|
+
});
|
|
2598
|
+
res.status(200).json(state);
|
|
2599
|
+
} catch (error: unknown) {
|
|
2600
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2601
|
+
logger.warn("init.state.failed", "init setup state failed", { errorMessage });
|
|
2602
|
+
res.status(500).json({
|
|
2603
|
+
error: {
|
|
2604
|
+
code: "init_state_failed",
|
|
2605
|
+
message: errorMessage
|
|
2606
|
+
}
|
|
2607
|
+
});
|
|
2608
|
+
}
|
|
2609
|
+
});
|
|
2610
|
+
|
|
2611
|
+
controlApp.post("/init/complete", (req, res) => {
|
|
2612
|
+
try {
|
|
2613
|
+
const completedSteps = assertInitSetupSteps(req.body?.completedSteps);
|
|
2614
|
+
const setup = buildCompletedInitSetupMarker(completedSteps);
|
|
2615
|
+
this.tokenStore.saveDaemonRuntimeConfig(INIT_SETUP_CONFIG_KEY, setup);
|
|
2616
|
+
const state = this.initStateSnapshot();
|
|
2617
|
+
logger.info("init.setup.completed", "init setup marker completed", {
|
|
2618
|
+
completedStepCount: setup.completedSteps.length
|
|
2619
|
+
});
|
|
2620
|
+
res.status(200).json(state);
|
|
2621
|
+
} catch (error: unknown) {
|
|
2622
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2623
|
+
logger.warn("init.setup.complete_failed", "init setup complete failed", { errorMessage });
|
|
2624
|
+
res.status(400).json({
|
|
2625
|
+
error: {
|
|
2626
|
+
code: "init_setup_complete_failed",
|
|
2627
|
+
message: errorMessage
|
|
2628
|
+
}
|
|
2629
|
+
});
|
|
2630
|
+
}
|
|
2631
|
+
});
|
|
2632
|
+
|
|
2633
|
+
controlApp.post("/init/doctor/run", async (_req, res) => {
|
|
2634
|
+
try {
|
|
2635
|
+
const report = await this.buildInitDoctorReport();
|
|
2636
|
+
logger.info("init.doctor.completed", "init doctor completed", {
|
|
2637
|
+
status: report.status,
|
|
2638
|
+
failedCount: report.checks.filter((check) => check.status === "failed").length,
|
|
2639
|
+
warningCount: report.checks.filter((check) => check.status === "warning").length
|
|
2640
|
+
});
|
|
2641
|
+
res.status(200).json(report);
|
|
2642
|
+
} catch (error: unknown) {
|
|
2643
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2644
|
+
logger.warn("init.doctor.failed", "init doctor failed", { errorMessage });
|
|
2645
|
+
res.status(500).json({
|
|
2646
|
+
error: {
|
|
2647
|
+
code: "init_doctor_failed",
|
|
2648
|
+
message: errorMessage
|
|
2649
|
+
}
|
|
2650
|
+
});
|
|
2651
|
+
}
|
|
2652
|
+
});
|
|
2653
|
+
|
|
2327
2654
|
controlApp.post("/payments/clawtip/activate", async (req, res) => {
|
|
2328
2655
|
try {
|
|
2329
2656
|
const qr = await this.startClawtipActivationQr();
|
|
@@ -2499,27 +2826,13 @@ export class TokenbuddyDaemon {
|
|
|
2499
2826
|
|
|
2500
2827
|
controlApp.get("/providers/status", (_req, res) => {
|
|
2501
2828
|
try {
|
|
2502
|
-
const
|
|
2503
|
-
const clients = [
|
|
2504
|
-
...providerStatuses,
|
|
2505
|
-
buildCustomClientToolStatus(this.activeProxyPort()),
|
|
2506
|
-
];
|
|
2507
|
-
const configuredCount = clients.filter((client) => client.configured).length;
|
|
2508
|
-
const detectedCount = clients.filter((client) => client.detected && client.status !== "manual").length;
|
|
2829
|
+
const status = this.clientToolsSummary();
|
|
2509
2830
|
logger.info("provider.status.requested", "provider status requested", {
|
|
2510
|
-
clientCount: clients.length,
|
|
2511
|
-
configuredCount,
|
|
2512
|
-
detectedCount
|
|
2513
|
-
});
|
|
2514
|
-
res.status(200).json({
|
|
2515
|
-
clients,
|
|
2516
|
-
summary: {
|
|
2517
|
-
configuredCount,
|
|
2518
|
-
detectedCount,
|
|
2519
|
-
totalCount: clients.length,
|
|
2520
|
-
installCommand: "tb init"
|
|
2521
|
-
}
|
|
2831
|
+
clientCount: status.clients.length,
|
|
2832
|
+
configuredCount: status.summary.configuredCount,
|
|
2833
|
+
detectedCount: status.summary.detectedCount
|
|
2522
2834
|
});
|
|
2835
|
+
res.status(200).json(status);
|
|
2523
2836
|
} catch (error: unknown) {
|
|
2524
2837
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2525
2838
|
logger.warn("provider.status.failed", "provider status failed", { errorMessage });
|
|
@@ -3098,6 +3411,58 @@ function routingKey(routing: BuyerSellerRoutingConfig): string {
|
|
|
3098
3411
|
].join("\u0001");
|
|
3099
3412
|
}
|
|
3100
3413
|
|
|
3414
|
+
function formatInitDoctorCatalogSource(source: InitDoctorCatalogSnapshot["source"]): string {
|
|
3415
|
+
if (source === "live") {
|
|
3416
|
+
return "实时注册表";
|
|
3417
|
+
}
|
|
3418
|
+
if (source === "cached") {
|
|
3419
|
+
return "本机缓存";
|
|
3420
|
+
}
|
|
3421
|
+
return "不可用";
|
|
3422
|
+
}
|
|
3423
|
+
|
|
3424
|
+
function sellerCatalogResultToRegistrySnapshot(catalog: Awaited<ReturnType<typeof discoverSellerBackedModels>>): SellerRegistryDocument {
|
|
3425
|
+
return {
|
|
3426
|
+
version: catalog.version,
|
|
3427
|
+
defaultSeller: catalog.defaultSeller,
|
|
3428
|
+
sellers: catalog.sellers
|
|
3429
|
+
.filter((seller) => seller.status === "ok")
|
|
3430
|
+
.map((seller) => ({
|
|
3431
|
+
id: seller.id,
|
|
3432
|
+
name: seller.name,
|
|
3433
|
+
status: "active",
|
|
3434
|
+
url: seller.url,
|
|
3435
|
+
supportedProtocols: seller.supportedProtocols,
|
|
3436
|
+
paymentMethods: seller.paymentMethods,
|
|
3437
|
+
models: catalog.models
|
|
3438
|
+
.filter((model) => model.sellerId === seller.id)
|
|
3439
|
+
.map((model) => model.id)
|
|
3440
|
+
}))
|
|
3441
|
+
};
|
|
3442
|
+
}
|
|
3443
|
+
|
|
3444
|
+
function catalogSnapshotFromRegistry(registry: SellerRegistryDocument): InitDoctorCatalogSnapshot {
|
|
3445
|
+
const visibleSellers = registry.sellers.filter(isBuyerVisibleRegistrySeller);
|
|
3446
|
+
return {
|
|
3447
|
+
available: visibleSellers.length > 0,
|
|
3448
|
+
source: "cached",
|
|
3449
|
+
models: visibleSellers.flatMap((seller) => (seller.models ?? []).map((modelId) => ({
|
|
3450
|
+
id: modelId,
|
|
3451
|
+
sellerId: seller.id,
|
|
3452
|
+
sellerName: seller.name,
|
|
3453
|
+
sellerUrl: seller.url,
|
|
3454
|
+
supportedProtocols: seller.supportedProtocols ?? [],
|
|
3455
|
+
paymentMethods: seller.paymentMethods ?? []
|
|
3456
|
+
}))),
|
|
3457
|
+
sellers: visibleSellers.map((seller) => ({
|
|
3458
|
+
id: seller.id,
|
|
3459
|
+
name: seller.name,
|
|
3460
|
+
url: seller.url,
|
|
3461
|
+
status: seller.status ?? "active"
|
|
3462
|
+
}))
|
|
3463
|
+
};
|
|
3464
|
+
}
|
|
3465
|
+
|
|
3101
3466
|
/**
|
|
3102
3467
|
* 从 query string 构造 `BuyerSellerRoutingConfig` override(用于 GET /routing/preview)。
|
|
3103
3468
|
* 接受 `mode` / `scorer` / `sellerId` / `sellerIds`(逗号分隔)。
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
export const INIT_SETUP_CONFIG_KEY = "init-setup";
|
|
2
|
+
|
|
3
|
+
export const INIT_SETUP_VERSION = 1;
|
|
4
|
+
|
|
5
|
+
export const INIT_SETUP_STEPS = [
|
|
6
|
+
"gateway_intro",
|
|
7
|
+
"model_access",
|
|
8
|
+
"supplier_routing",
|
|
9
|
+
"auto_purchase",
|
|
10
|
+
"connect_tools",
|
|
11
|
+
"verify_gateway",
|
|
12
|
+
"install_app",
|
|
13
|
+
] as const;
|
|
14
|
+
|
|
15
|
+
export const DEFAULT_INIT_RECOMMENDED_MODELS = [
|
|
16
|
+
"deepseek-v4-flash",
|
|
17
|
+
"kimi-2.6",
|
|
18
|
+
"gpt-5.5",
|
|
19
|
+
"gpt-5.4",
|
|
20
|
+
"claude-opus-4.7",
|
|
21
|
+
"claude-opus-4.8",
|
|
22
|
+
"claude-sonnet-4.6",
|
|
23
|
+
"gemma-4-26b-a4b-it",
|
|
24
|
+
"qwen3-235b-a22b",
|
|
25
|
+
"google/gemini-3.5-flash",
|
|
26
|
+
"google/gemini-3.1-pro-preview",
|
|
27
|
+
"qwen/qwen3.7-max",
|
|
28
|
+
"qwen/qwen3.7-plus",
|
|
29
|
+
] as const;
|
|
30
|
+
|
|
31
|
+
export type InitSetupStep = typeof INIT_SETUP_STEPS[number];
|
|
32
|
+
export type InitSetupStatus = "not_started" | "in_progress" | "completed";
|
|
33
|
+
|
|
34
|
+
export interface InitSetupMarker {
|
|
35
|
+
status: InitSetupStatus;
|
|
36
|
+
version: typeof INIT_SETUP_VERSION;
|
|
37
|
+
completedSteps: InitSetupStep[];
|
|
38
|
+
startedAt?: string;
|
|
39
|
+
updatedAt?: string;
|
|
40
|
+
completedAt?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const INIT_SETUP_STEP_SET = new Set<string>(INIT_SETUP_STEPS);
|
|
44
|
+
|
|
45
|
+
export function normalizeInitSetupMarker(value: unknown): InitSetupMarker {
|
|
46
|
+
if (!value || typeof value !== "object") {
|
|
47
|
+
return defaultInitSetupMarker();
|
|
48
|
+
}
|
|
49
|
+
const raw = value as {
|
|
50
|
+
status?: unknown;
|
|
51
|
+
completedSteps?: unknown;
|
|
52
|
+
startedAt?: unknown;
|
|
53
|
+
updatedAt?: unknown;
|
|
54
|
+
completedAt?: unknown;
|
|
55
|
+
};
|
|
56
|
+
const status = normalizeInitSetupStatus(raw.status);
|
|
57
|
+
return {
|
|
58
|
+
status,
|
|
59
|
+
version: INIT_SETUP_VERSION,
|
|
60
|
+
completedSteps: normalizeInitSetupSteps(raw.completedSteps),
|
|
61
|
+
startedAt: typeof raw.startedAt === "string" ? raw.startedAt : undefined,
|
|
62
|
+
updatedAt: typeof raw.updatedAt === "string" ? raw.updatedAt : undefined,
|
|
63
|
+
completedAt: typeof raw.completedAt === "string" ? raw.completedAt : undefined,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function buildInProgressInitSetupMarker(
|
|
68
|
+
previous: InitSetupMarker = defaultInitSetupMarker(),
|
|
69
|
+
now: Date = new Date(),
|
|
70
|
+
): InitSetupMarker {
|
|
71
|
+
const updatedAt = now.toISOString();
|
|
72
|
+
return {
|
|
73
|
+
...previous,
|
|
74
|
+
status: previous.status === "completed" ? "completed" : "in_progress",
|
|
75
|
+
version: INIT_SETUP_VERSION,
|
|
76
|
+
startedAt: previous.startedAt || updatedAt,
|
|
77
|
+
updatedAt,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function buildCompletedInitSetupMarker(
|
|
82
|
+
completedSteps: readonly string[] = INIT_SETUP_STEPS,
|
|
83
|
+
now: Date = new Date(),
|
|
84
|
+
): InitSetupMarker {
|
|
85
|
+
const normalizedSteps = normalizeInitSetupSteps(completedSteps);
|
|
86
|
+
const updatedAt = now.toISOString();
|
|
87
|
+
return {
|
|
88
|
+
status: "completed",
|
|
89
|
+
version: INIT_SETUP_VERSION,
|
|
90
|
+
completedSteps: normalizedSteps.length > 0 ? normalizedSteps : [...INIT_SETUP_STEPS],
|
|
91
|
+
startedAt: updatedAt,
|
|
92
|
+
updatedAt,
|
|
93
|
+
completedAt: updatedAt,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function assertInitSetupSteps(value: unknown): InitSetupStep[] {
|
|
98
|
+
if (value === undefined) {
|
|
99
|
+
return [...INIT_SETUP_STEPS];
|
|
100
|
+
}
|
|
101
|
+
if (!Array.isArray(value)) {
|
|
102
|
+
throw new Error("completedSteps must be an array");
|
|
103
|
+
}
|
|
104
|
+
const steps: InitSetupStep[] = [];
|
|
105
|
+
for (const entry of value) {
|
|
106
|
+
if (typeof entry !== "string" || !INIT_SETUP_STEP_SET.has(entry)) {
|
|
107
|
+
throw new Error(`unknown init setup step: ${String(entry)}`);
|
|
108
|
+
}
|
|
109
|
+
if (!steps.includes(entry as InitSetupStep)) {
|
|
110
|
+
steps.push(entry as InitSetupStep);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return steps;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function isFreshInitMachine(marker: InitSetupMarker): boolean {
|
|
117
|
+
return marker.status !== "completed";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function normalizeInitRecommendedModels(value: readonly string[] | string | undefined): string[] {
|
|
121
|
+
const entries = typeof value === "string"
|
|
122
|
+
? value.split(",")
|
|
123
|
+
: value ?? [];
|
|
124
|
+
return Array.from(new Set(entries.map((entry) => entry.trim()).filter(Boolean)));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function resolveInitRecommendedModels(input?: {
|
|
128
|
+
configuredModels?: readonly string[];
|
|
129
|
+
env?: Record<string, string | undefined>;
|
|
130
|
+
}): string[] {
|
|
131
|
+
const configured = normalizeInitRecommendedModels(input?.configuredModels);
|
|
132
|
+
if (configured.length > 0) {
|
|
133
|
+
return configured;
|
|
134
|
+
}
|
|
135
|
+
const envModels = normalizeInitRecommendedModels(input?.env?.TB_PROXYD_INIT_RECOMMENDED_MODELS);
|
|
136
|
+
if (envModels.length > 0) {
|
|
137
|
+
return envModels;
|
|
138
|
+
}
|
|
139
|
+
return [...DEFAULT_INIT_RECOMMENDED_MODELS];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function defaultInitSetupMarker(): InitSetupMarker {
|
|
143
|
+
return {
|
|
144
|
+
status: "not_started",
|
|
145
|
+
version: INIT_SETUP_VERSION,
|
|
146
|
+
completedSteps: [],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function normalizeInitSetupStatus(value: unknown): InitSetupStatus {
|
|
151
|
+
return value === "in_progress" || value === "completed" ? value : "not_started";
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function normalizeInitSetupSteps(value: unknown): InitSetupStep[] {
|
|
155
|
+
if (!Array.isArray(value)) {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
const steps: InitSetupStep[] = [];
|
|
159
|
+
for (const entry of value) {
|
|
160
|
+
if (typeof entry === "string" && INIT_SETUP_STEP_SET.has(entry) && !steps.includes(entry as InitSetupStep)) {
|
|
161
|
+
steps.push(entry as InitSetupStep);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return steps;
|
|
165
|
+
}
|