mycomesh-consumer 0.1.50 → 0.1.51
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/README.md +14 -8
- package/package.json +1 -1
- package/src/consumer-runtime.mjs +182 -55
- package/src/consumer.mjs +17 -5
package/README.md
CHANGED
|
@@ -25,16 +25,19 @@ The command starts the Consumer API on `127.0.0.1:8110`, prints and optionally
|
|
|
25
25
|
opens the local credentials page, and stays available for any
|
|
26
26
|
OpenAI-compatible client. It does not start or configure Codex.
|
|
27
27
|
|
|
28
|
-
The page
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
The page starts with a wallet signature check. The selected wallet must match
|
|
29
|
+
the local payment key's active on-chain grant before the API key/export and
|
|
30
|
+
inference are enabled. It exposes only the local API URL, payment key/export,
|
|
31
|
+
prepaid balance, key operations, and local consumption history. It has no
|
|
32
|
+
conversation or request-session UI. The payment key is generated once and stored at
|
|
31
33
|
`~/.mycomesh/consumer/payment-key` with mode `0600`; set
|
|
32
34
|
`MYCOMESH_CONSUMER_DATA_DIR` to choose another directory.
|
|
33
35
|
|
|
34
36
|
## Connect a client
|
|
35
37
|
|
|
36
|
-
The default command is already service-only.
|
|
37
|
-
shell and
|
|
38
|
+
The default command is already service-only. Sign in on the local page first,
|
|
39
|
+
then load the export block into a shell and use Codex or another compatible
|
|
40
|
+
client:
|
|
38
41
|
|
|
39
42
|
```sh
|
|
40
43
|
mycomesh-consumer --no-browser
|
|
@@ -63,7 +66,9 @@ export OPENAI_API_KEY='myco_sk_...'
|
|
|
63
66
|
mycomesh responses --input 'hello' --model mycomesh-codex-standard-v1
|
|
64
67
|
```
|
|
65
68
|
|
|
66
|
-
The browser's export block is the canonical way to obtain both values
|
|
69
|
+
The browser's export block is the canonical way to obtain both values after
|
|
70
|
+
wallet verification. `--codex` waits for that verification before launching
|
|
71
|
+
Codex.
|
|
67
72
|
|
|
68
73
|
## Temporary sharing
|
|
69
74
|
|
|
@@ -81,8 +86,9 @@ The Consumer checks each configured Relay's V8 health and automatically tries
|
|
|
81
86
|
the next Relay after a health, timeout, or retryable HTTP failure. A single
|
|
82
87
|
request ID is preserved across failover. Each attempt carries a fresh V8
|
|
83
88
|
EIP-712 payment authorization signed by the persisted key; the Relay resolves
|
|
84
|
-
the key address to its on-chain grant and settles the signed receipt.
|
|
85
|
-
signature
|
|
89
|
+
the key address to its on-chain grant and settles the signed receipt. A wallet
|
|
90
|
+
signature unlocks each Consumer process once; inference has no per-request
|
|
91
|
+
wallet signature or Consumer-managed conversation state.
|
|
86
92
|
|
|
87
93
|
Configure multiple Relay origins and an optional outbound proxy:
|
|
88
94
|
|
package/package.json
CHANGED
package/src/consumer-runtime.mjs
CHANGED
|
@@ -88,6 +88,7 @@ const TUNNEL_STOP_TIMEOUT_MS = 2_000;
|
|
|
88
88
|
const RELAY_HEALTH_CACHE_MS = 30_000;
|
|
89
89
|
const RELAY_HEALTH_STALE_MS = 10 * 60_000;
|
|
90
90
|
const RELAY_HEALTH_RETRY_DELAY_MS = 250;
|
|
91
|
+
const WALLET_CHALLENGE_TTL_MS = 5 * 60_000;
|
|
91
92
|
|
|
92
93
|
const DEFAULT_NETWORK = Object.freeze({
|
|
93
94
|
chain_id: DEFAULT_CHAIN_ID,
|
|
@@ -275,6 +276,12 @@ function recoverAddress(digest, signatureValue) {
|
|
|
275
276
|
return `0x${bytesToHex(keccak_256(publicKey).slice(-20))}`;
|
|
276
277
|
}
|
|
277
278
|
|
|
279
|
+
export function walletMessageDigest(message) {
|
|
280
|
+
const body = Buffer.from(String(message), "utf8");
|
|
281
|
+
const prefix = Buffer.from(`\x19Ethereum Signed Message:\n${body.length}`, "utf8");
|
|
282
|
+
return Uint8Array.from(keccak_256(Buffer.concat([prefix, body])));
|
|
283
|
+
}
|
|
284
|
+
|
|
278
285
|
function typedDigest(structHash, chainId, contract) {
|
|
279
286
|
const domain = keccak_256(
|
|
280
287
|
Buffer.concat([
|
|
@@ -631,6 +638,10 @@ export class NativeConsumerState {
|
|
|
631
638
|
this.tunnelCommand = options.tunnelCommand || env.MYCOMESH_CONSUMER_TUNNEL_COMMAND || "cloudflared";
|
|
632
639
|
this.tunnelSpawn = options.tunnelSpawn || defaultSpawn;
|
|
633
640
|
this.share = null;
|
|
641
|
+
this.walletChallenge = null;
|
|
642
|
+
this.managementToken = null;
|
|
643
|
+
this.unlockedWallet = null;
|
|
644
|
+
this.paymentUnlocked = false;
|
|
634
645
|
}
|
|
635
646
|
|
|
636
647
|
loadPaymentKey(configured) {
|
|
@@ -667,10 +678,87 @@ export class NativeConsumerState {
|
|
|
667
678
|
relay_urls: this.relayUrls,
|
|
668
679
|
payment_key_address: this.paymentAddress,
|
|
669
680
|
payment_key_persisted: !this.paymentKeyFromEnv,
|
|
681
|
+
wallet_unlocked: this.paymentUnlocked,
|
|
670
682
|
responses_transports: ["http", "sse"],
|
|
671
683
|
};
|
|
672
684
|
}
|
|
673
685
|
|
|
686
|
+
walletAuthPayload() {
|
|
687
|
+
return {
|
|
688
|
+
authenticated: Boolean(this.managementToken && this.unlockedWallet),
|
|
689
|
+
wallet: this.unlockedWallet,
|
|
690
|
+
key_ready: this.paymentUnlocked,
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
createWalletChallenge(walletValue) {
|
|
695
|
+
const wallet = normalizeAddress(walletValue, "wallet");
|
|
696
|
+
const issuedAt = Date.now();
|
|
697
|
+
const expiresAt = issuedAt + WALLET_CHALLENGE_TTL_MS;
|
|
698
|
+
const nonce = randomBytes(24).toString("base64url");
|
|
699
|
+
const message = [
|
|
700
|
+
"MycoMesh Consumer wallet login",
|
|
701
|
+
`Wallet: ${wallet}`,
|
|
702
|
+
`Payment key: ${this.paymentAddress}`,
|
|
703
|
+
`Nonce: ${nonce}`,
|
|
704
|
+
`Issued at: ${new Date(issuedAt).toISOString()}`,
|
|
705
|
+
"This signature unlocks this local Consumer process only.",
|
|
706
|
+
].join("\n");
|
|
707
|
+
this.walletChallenge = { wallet, message, expiresAt };
|
|
708
|
+
return { wallet, key_address: this.paymentAddress, message, expires_at: Math.floor(expiresAt / 1000) };
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
async authenticateWallet(raw) {
|
|
712
|
+
const wallet = normalizeAddress(raw?.wallet, "wallet");
|
|
713
|
+
const challenge = this.walletChallenge;
|
|
714
|
+
if (!challenge || challenge.wallet !== wallet || challenge.expiresAt < Date.now()) {
|
|
715
|
+
this.walletChallenge = null;
|
|
716
|
+
throw new Error("wallet login challenge is missing or expired");
|
|
717
|
+
}
|
|
718
|
+
this.walletChallenge = null;
|
|
719
|
+
const recovered = recoverAddress(walletMessageDigest(challenge.message), raw?.signature);
|
|
720
|
+
if (recovered !== wallet) throw new Error("wallet signature does not match the selected account");
|
|
721
|
+
const grant = await this.keyGrant(this.paymentAddress);
|
|
722
|
+
if (grant.owner !== ZERO_ADDRESS && grant.owner !== wallet) {
|
|
723
|
+
throw new Error("this local payment key belongs to a different wallet");
|
|
724
|
+
}
|
|
725
|
+
this.unlockedWallet = wallet;
|
|
726
|
+
this.managementToken = `myco_local_${randomBytes(32).toString("base64url")}`;
|
|
727
|
+
this.paymentUnlocked = grant.active && grant.owner === wallet;
|
|
728
|
+
return { ok: true, token: this.managementToken, auth: this.walletAuthPayload(), grant };
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
authorizeManagement(authorization) {
|
|
732
|
+
return Boolean(this.managementToken && sameSecret(authorization, `Bearer ${this.managementToken}`));
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
assertUnlockedWallet(walletValue = this.unlockedWallet) {
|
|
736
|
+
const wallet = normalizeAddress(walletValue, "wallet");
|
|
737
|
+
if (!this.managementToken || wallet !== this.unlockedWallet) {
|
|
738
|
+
throw new Error("sign in with the payment-key owner wallet first");
|
|
739
|
+
}
|
|
740
|
+
return wallet;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
async activateCurrentPaymentKey() {
|
|
744
|
+
const wallet = this.assertUnlockedWallet();
|
|
745
|
+
const grant = await this.keyGrant(this.paymentAddress);
|
|
746
|
+
if (!grant.active || grant.owner !== wallet) {
|
|
747
|
+
throw new Error("the payment key is not active for this wallet on-chain");
|
|
748
|
+
}
|
|
749
|
+
this.paymentUnlocked = true;
|
|
750
|
+
return { ok: true, auth: this.walletAuthPayload(), grant };
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
async lockWallet() {
|
|
754
|
+
this.walletChallenge = null;
|
|
755
|
+
this.managementToken = null;
|
|
756
|
+
this.unlockedWallet = null;
|
|
757
|
+
this.paymentUnlocked = false;
|
|
758
|
+
await this.stopShare();
|
|
759
|
+
return { ok: true, auth: this.walletAuthPayload() };
|
|
760
|
+
}
|
|
761
|
+
|
|
674
762
|
history(limit = 100) {
|
|
675
763
|
if (!existsSync(this.historyPath)) return [];
|
|
676
764
|
const lines = readFileSync(this.historyPath, "utf8").split("\n").filter(Boolean);
|
|
@@ -746,36 +834,45 @@ export class NativeConsumerState {
|
|
|
746
834
|
return BigInt(output || "0x0").toString();
|
|
747
835
|
}
|
|
748
836
|
|
|
749
|
-
async dashboardPayload(
|
|
837
|
+
async dashboardPayload(managementAuthorized = false) {
|
|
838
|
+
const authenticated = managementAuthorized && Boolean(this.unlockedWallet);
|
|
750
839
|
const allHistory = this.history(0);
|
|
840
|
+
const pending = authenticated ? this.pendingPaymentKey() : null;
|
|
751
841
|
const payload = {
|
|
752
842
|
ok: true,
|
|
753
843
|
protocol_version: 8,
|
|
754
844
|
runtime: "node-native",
|
|
755
|
-
|
|
756
|
-
|
|
845
|
+
auth: authenticated ? this.walletAuthPayload() : { authenticated: false, wallet: null, key_ready: false },
|
|
846
|
+
credentials: authenticated && this.paymentUnlocked
|
|
847
|
+
? { base_url: this.baseUrl, api_key: this.paymentKey, export: this.credentialsText() }
|
|
848
|
+
: null,
|
|
849
|
+
key: {
|
|
850
|
+
address: this.paymentAddress,
|
|
851
|
+
max_fee_units: this.maxFeeUnits,
|
|
852
|
+
pending: pending ? { payment_key_address: pending.payment_key_address } : null,
|
|
853
|
+
},
|
|
757
854
|
settlement: this.network,
|
|
758
|
-
history: allHistory.slice(0, 100),
|
|
855
|
+
history: authenticated ? allHistory.slice(0, 100) : [],
|
|
759
856
|
usage: {
|
|
760
|
-
request_count: allHistory.length,
|
|
761
|
-
total_spent_units: allHistory.reduce((total, item) => total + Number(item.actual_fee_units || 0), 0),
|
|
762
|
-
input_tokens: allHistory.reduce((total, item) => total + Number(item.input_tokens || 0), 0),
|
|
763
|
-
output_tokens: allHistory.reduce((total, item) => total + Number(item.output_tokens || 0), 0),
|
|
857
|
+
request_count: authenticated ? allHistory.length : 0,
|
|
858
|
+
total_spent_units: authenticated ? allHistory.reduce((total, item) => total + Number(item.actual_fee_units || 0), 0) : 0,
|
|
859
|
+
input_tokens: authenticated ? allHistory.reduce((total, item) => total + Number(item.input_tokens || 0), 0) : 0,
|
|
860
|
+
output_tokens: authenticated ? allHistory.reduce((total, item) => total + Number(item.output_tokens || 0), 0) : 0,
|
|
764
861
|
},
|
|
765
|
-
share: this.sharePayload(),
|
|
862
|
+
share: authenticated ? this.sharePayload() : { active: false },
|
|
766
863
|
};
|
|
767
864
|
try {
|
|
768
865
|
const grant = await this.keyGrant(this.paymentAddress);
|
|
769
866
|
payload.key.grant = grant;
|
|
770
|
-
if (grant.owner !== ZERO_ADDRESS) {
|
|
867
|
+
if (authenticated && grant.owner !== ZERO_ADDRESS) {
|
|
771
868
|
payload.account = { owner: grant.owner, available_balance_units: await this.accountBalance(grant.owner) };
|
|
772
869
|
}
|
|
773
870
|
} catch (error) {
|
|
774
871
|
payload.chain_error = error.message;
|
|
775
872
|
}
|
|
776
|
-
if (
|
|
873
|
+
if (authenticated) {
|
|
777
874
|
try {
|
|
778
|
-
const address =
|
|
875
|
+
const address = this.unlockedWallet;
|
|
779
876
|
const token = await this.rpcValue((rpc) => this.contractCall(rpc, this.network.stablecoin, "balanceOf(address)", [address]));
|
|
780
877
|
const allowance = await this.rpcValue((rpc) => this.contractCall(rpc, this.network.stablecoin, "allowance(address,address)", [address, this.network.settlement_contract]));
|
|
781
878
|
payload.wallet = { address, token_balance_units: BigInt(token || "0x0").toString(), allowance_units: BigInt(allowance || "0x0").toString() };
|
|
@@ -805,12 +902,13 @@ export class NativeConsumerState {
|
|
|
805
902
|
}
|
|
806
903
|
|
|
807
904
|
authorizeBearer(authorization, { shareOnly = false } = {}) {
|
|
808
|
-
if (!shareOnly && sameSecret(authorization, `Bearer ${this.paymentKey}`)) return true;
|
|
905
|
+
if (!shareOnly && this.paymentUnlocked && sameSecret(authorization, `Bearer ${this.paymentKey}`)) return true;
|
|
809
906
|
const share = this.activeShare();
|
|
810
907
|
return Boolean(share && sameSecret(authorization, `Bearer ${share.apiKey}`));
|
|
811
908
|
}
|
|
812
909
|
|
|
813
910
|
async startShare(minutesValue) {
|
|
911
|
+
if (!this.paymentUnlocked) throw new Error("sign in and activate the payment key before sharing");
|
|
814
912
|
const minutes = Number(minutesValue);
|
|
815
913
|
if (!Number.isInteger(minutes) || minutes < 1 || minutes > MAX_SHARE_MINUTES) {
|
|
816
914
|
throw new Error(`share duration must be between 1 and ${MAX_SHARE_MINUTES} minutes`);
|
|
@@ -889,7 +987,7 @@ export class NativeConsumerState {
|
|
|
889
987
|
}
|
|
890
988
|
|
|
891
989
|
async activatePendingPaymentKey(wallet) {
|
|
892
|
-
const owner =
|
|
990
|
+
const owner = this.assertUnlockedWallet(wallet);
|
|
893
991
|
const pending = this.pendingPaymentKey();
|
|
894
992
|
if (!pending) throw new Error("no pending payment key exists");
|
|
895
993
|
const grant = await this.keyGrant(pending.payment_key_address);
|
|
@@ -901,12 +999,13 @@ export class NativeConsumerState {
|
|
|
901
999
|
unlinkSync(this.pendingKeyPath);
|
|
902
1000
|
this.paymentKey = pending.payment_key;
|
|
903
1001
|
this.paymentAddress = pending.payment_key_address;
|
|
904
|
-
|
|
1002
|
+
this.paymentUnlocked = true;
|
|
1003
|
+
return { payment_key_address: this.paymentAddress, previous_key_address: previous };
|
|
905
1004
|
}
|
|
906
1005
|
|
|
907
1006
|
async transactionPlan(raw) {
|
|
908
1007
|
const action = String(raw?.action || "");
|
|
909
|
-
const wallet =
|
|
1008
|
+
const wallet = this.assertUnlockedWallet(raw?.wallet);
|
|
910
1009
|
const settlement = this.network.settlement_contract;
|
|
911
1010
|
const token = this.network.stablecoin;
|
|
912
1011
|
if (action === "top_up") {
|
|
@@ -1315,6 +1414,7 @@ export function createConsumerServer(state, { host = "127.0.0.1", port = 8110, p
|
|
|
1315
1414
|
return;
|
|
1316
1415
|
}
|
|
1317
1416
|
if (request.method === "GET" && (path === "/credentials" || path === "/codex-env")) {
|
|
1417
|
+
if (!state.paymentUnlocked) { writeJson(response, 423, { ok: false, error: "sign in with the payment-key owner wallet first" }); return; }
|
|
1318
1418
|
response.writeHead(200, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" }); response.end(`${state.credentialsText()}\n`); return;
|
|
1319
1419
|
}
|
|
1320
1420
|
if (request.method === "GET" && (path === "/models" || path === "/v1/models" || path === "/backend-api/codex/models")) {
|
|
@@ -1322,31 +1422,54 @@ export function createConsumerServer(state, { host = "127.0.0.1", port = 8110, p
|
|
|
1322
1422
|
catch (error) { writeJson(response, 503, openaiError(error.message, "relay_unavailable")); }
|
|
1323
1423
|
return;
|
|
1324
1424
|
}
|
|
1325
|
-
if (request.method === "
|
|
1425
|
+
if (request.method === "POST" && path === "/v1/mycomesh/local/wallet/challenge") {
|
|
1426
|
+
try { const value = await decodeRequestBody(request); writeJson(response, 200, state.createWalletChallenge(value.wallet)); }
|
|
1427
|
+
catch (error) { writeJson(response, 400, { ok: false, error: error.message }); }
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
if (request.method === "POST" && path === "/v1/mycomesh/local/wallet/authenticate") {
|
|
1431
|
+
try { writeJson(response, 200, await state.authenticateWallet(await decodeRequestBody(request))); }
|
|
1432
|
+
catch (error) { writeJson(response, 401, { ok: false, error: error.message }); }
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
if (request.method === "GET" && path === "/v1/mycomesh/local/dashboard") {
|
|
1436
|
+
writeJson(response, 200, await state.dashboardPayload(state.authorizeManagement(String(request.headers.authorization || "")))); return;
|
|
1437
|
+
}
|
|
1438
|
+
if (request.method === "POST" && path === "/v1/mycomesh/local/wallet/activate") {
|
|
1439
|
+
if (!state.authorizeManagement(String(request.headers.authorization || ""))) { writeJson(response, 401, { ok: false, error: "wallet login required" }); return; }
|
|
1440
|
+
try { writeJson(response, 200, await state.activateCurrentPaymentKey()); }
|
|
1441
|
+
catch (error) { writeJson(response, 400, { ok: false, error: error.message }); }
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
if (request.method === "POST" && path === "/v1/mycomesh/local/wallet/lock") {
|
|
1445
|
+
if (!state.authorizeManagement(String(request.headers.authorization || ""))) { writeJson(response, 401, { ok: false, error: "wallet login required" }); return; }
|
|
1446
|
+
writeJson(response, 200, await state.lockWallet()); return;
|
|
1447
|
+
}
|
|
1326
1448
|
if (request.method === "POST" && path === "/v1/mycomesh/local/transactions") {
|
|
1449
|
+
if (!state.authorizeManagement(String(request.headers.authorization || ""))) { writeJson(response, 401, { ok: false, error: "wallet login required" }); return; }
|
|
1327
1450
|
try { writeJson(response, 200, await state.transactionPlan(await decodeRequestBody(request))); }
|
|
1328
1451
|
catch (error) { writeJson(response, 400, { ok: false, error: error.message }); }
|
|
1329
1452
|
return;
|
|
1330
1453
|
}
|
|
1331
1454
|
if (request.method === "POST" && path === "/v1/mycomesh/local/key/prepare") {
|
|
1332
|
-
if (!
|
|
1455
|
+
if (!state.authorizeManagement(String(request.headers.authorization || ""))) { writeJson(response, 401, { ok: false, error: "wallet login required" }); return; }
|
|
1333
1456
|
try { writeJson(response, 200, state.preparePaymentKey()); } catch (error) { writeJson(response, 400, { ok: false, error: error.message }); }
|
|
1334
1457
|
return;
|
|
1335
1458
|
}
|
|
1336
1459
|
if (request.method === "POST" && path === "/v1/mycomesh/local/key/activate") {
|
|
1337
|
-
if (!
|
|
1460
|
+
if (!state.authorizeManagement(String(request.headers.authorization || ""))) { writeJson(response, 401, { ok: false, error: "wallet login required" }); return; }
|
|
1338
1461
|
try { const value = await decodeRequestBody(request); writeJson(response, 200, await state.activatePendingPaymentKey(value.wallet)); }
|
|
1339
1462
|
catch (error) { writeJson(response, 400, { ok: false, error: error.message }); }
|
|
1340
1463
|
return;
|
|
1341
1464
|
}
|
|
1342
1465
|
if (request.method === "POST" && path === "/v1/mycomesh/local/share/start") {
|
|
1343
|
-
if (!
|
|
1466
|
+
if (!state.authorizeManagement(String(request.headers.authorization || ""))) { writeJson(response, 401, { ok: false, error: "wallet login required" }); return; }
|
|
1344
1467
|
try { const value = await decodeRequestBody(request); writeJson(response, 200, { ok: true, share: await state.startShare(value.minutes) }); }
|
|
1345
1468
|
catch (error) { writeJson(response, 400, { ok: false, error: error.message }); }
|
|
1346
1469
|
return;
|
|
1347
1470
|
}
|
|
1348
1471
|
if (request.method === "POST" && path === "/v1/mycomesh/local/share/stop") {
|
|
1349
|
-
if (!
|
|
1472
|
+
if (!state.authorizeManagement(String(request.headers.authorization || ""))) { writeJson(response, 401, { ok: false, error: "wallet login required" }); return; }
|
|
1350
1473
|
writeJson(response, 200, { ok: true, share: await state.stopShare() });
|
|
1351
1474
|
return;
|
|
1352
1475
|
}
|
|
@@ -1472,44 +1595,48 @@ export function chatCompletionSse(payload, includeUsage = false) {
|
|
|
1472
1595
|
|
|
1473
1596
|
function consumerHtml() {
|
|
1474
1597
|
return `<!doctype html>
|
|
1475
|
-
<html lang="
|
|
1476
|
-
<title>MycoMesh
|
|
1598
|
+
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><meta name="color-scheme" content="light">
|
|
1599
|
+
<title>Consumer | MycoMesh</title>
|
|
1477
1600
|
<style>
|
|
1478
|
-
body{font:
|
|
1479
|
-
h1{margin-bottom:4px}.muted{color:#68736e}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:14px}
|
|
1480
|
-
.panel{background:white;border:1px solid #d9e0dc;border-radius:6px;padding:18px;margin:14px 0}.value{word-break:break-all;background:#f1f4f2;padding:10px;border-radius:4px;font:12px monospace;white-space:pre-wrap}
|
|
1481
|
-
button{padding:9px 12px;border:1px solid #177b57;border-radius:4px;background:#177b57;color:#fff;cursor:pointer}input,select{padding:9px;border:1px solid #c7d0ca;border-radius:4px;background:#fff}.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
|
1482
|
-
table{width:100%;border-collapse:collapse}td,th{padding:8px;border-bottom:1px solid #e1e7e3;text-align:left;font-size:13px}
|
|
1601
|
+
:root{--ink:#17211d;--muted:#68736e;--line:#d8dfdb;--soft:#f2f5f3;--paper:#fff;--green:#147553;--green-dark:#0d5b40;--amber:#9a6413;--red:#ae3d38;--blue:#365d86}*{box-sizing:border-box}[hidden]{display:none!important}html{background:#edf1ee}body{min-width:320px;margin:0;color:var(--ink);background:#edf1ee;font:14px/1.5 Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;letter-spacing:0}button,input,select{font:inherit;letter-spacing:0}button{cursor:pointer}.shell{min-height:100vh}.topbar{position:sticky;z-index:10;top:0;display:flex;min-height:60px;align-items:center;justify-content:space-between;border-bottom:1px solid var(--line);background:rgba(255,255,255,.97);padding:0 max(18px,env(safe-area-inset-left))}.brand{display:flex;align-items:center;gap:10px;font-weight:780}.mark{display:grid;width:30px;height:30px;place-items:center;border-radius:6px;background:var(--ink);color:#fff;font-size:12px}.network{display:flex;align-items:center;gap:7px;color:var(--muted);font-size:12px}.dot{width:7px;height:7px;border-radius:50%;background:#9ba49f}.dot.ok{background:var(--green)}.workspace{width:min(760px,100%);margin:0 auto;background:var(--paper);min-height:calc(100vh - 60px)}.locked{display:grid;min-height:calc(100vh - 60px);align-content:center;padding:42px 24px 88px}.locked-inner{width:min(420px,100%);margin:0 auto}.locked-badge{display:inline-flex;align-items:center;border:1px solid var(--line);border-radius:999px;padding:5px 10px;color:var(--muted);font-size:12px}.locked h1{margin:20px 0 9px;font-size:30px;line-height:1.16}.locked p{margin:0 0 26px;color:var(--muted)}.key-preview{border-top:1px solid var(--line);border-bottom:1px solid var(--line);padding:14px 0;margin:0 0 22px}.label{display:block;color:var(--muted);font-size:11px;font-weight:700;text-transform:uppercase}.mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;word-break:break-all}.key-preview .mono{display:block;margin-top:5px;font-size:12px}.button{display:inline-flex;min-height:42px;align-items:center;justify-content:center;border:1px solid var(--line);border-radius:6px;background:var(--paper);padding:0 15px;color:var(--ink);font-weight:700}.button:hover{border-color:#95a39b;background:#f8faf9}.button.primary{border-color:var(--green);background:var(--green);color:#fff}.button.primary:hover{background:var(--green-dark)}.button.danger{border-color:#e5b6b3;color:var(--red)}.button.small{min-height:34px;padding:0 11px;font-size:12px}.button:disabled{cursor:not-allowed;opacity:.52}.locked .button{width:100%;min-height:48px}.app-head{padding:24px 20px 17px}.app-head-row{display:flex;align-items:flex-start;justify-content:space-between;gap:16px}.app-head h1{margin:0;font-size:22px}.wallet-button{max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.balance{margin-top:22px}.balance strong{display:block;margin-top:4px;font-size:31px;font-weight:760}.balance-meta{display:flex;gap:16px;margin-top:8px;color:var(--muted);font-size:12px}.tabs{position:sticky;z-index:8;top:60px;display:grid;grid-template-columns:repeat(4,1fr);border-top:1px solid var(--line);border-bottom:1px solid var(--line);background:rgba(255,255,255,.97);padding:0 12px}.tab{min-width:0;border:0;border-bottom:2px solid transparent;background:transparent;padding:12px 4px;color:var(--muted);font-weight:650}.tab.active{border-color:var(--green);color:var(--ink)}.view{padding:2px 20px 84px}.band{border-top:1px solid var(--line);padding:23px 0}.band:first-child{border-top:0}.section-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:14px}.section-head h2{margin:0;font-size:16px}.section-head p{margin:3px 0 0;color:var(--muted);font-size:12px}.status{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--line);border-radius:999px;padding:4px 9px;color:var(--muted);font-size:11px;white-space:nowrap}.status:before{width:6px;height:6px;border-radius:50%;background:var(--amber);content:""}.status.ok:before{background:var(--green)}.field+.field{margin-top:13px}.field-row{display:flex;align-items:stretch;gap:7px;margin-top:6px}.value{min-width:0;flex:1;border:1px solid var(--line);border-radius:5px;background:var(--soft);padding:10px 11px;font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;word-break:break-all}.value.exports{min-height:76px;white-space:pre-wrap}.metrics{display:grid;grid-template-columns:repeat(3,1fr);border:1px solid var(--line);border-radius:6px;overflow:hidden}.metric{min-width:0;padding:14px 10px;border-right:1px solid var(--line)}.metric:last-child{border-right:0}.metric span{display:block;color:var(--muted);font-size:11px}.metric strong{display:block;overflow:hidden;margin-top:4px;font-size:17px;text-overflow:ellipsis}.list{margin:0}.list div{display:grid;grid-template-columns:104px minmax(0,1fr);gap:12px;border-bottom:1px solid var(--line);padding:12px 0}.list div:last-child{border-bottom:0}.list dt{color:var(--muted)}.list dd{overflow:hidden;margin:0;text-align:right;text-overflow:ellipsis;white-space:nowrap}.actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:16px}.topup{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px}.input,.select{width:100%;min-height:42px;border:1px solid var(--line);border-radius:5px;background:#fff;padding:8px 10px;color:var(--ink)}.input:focus,.select:focus{border-color:var(--green);outline:2px solid rgba(20,117,83,.13)}.share-output{margin-top:16px}.empty{padding:34px 0;color:var(--muted);text-align:center}.table-wrap{overflow-x:auto;border-top:1px solid var(--line)}table{width:100%;min-width:620px;border-collapse:collapse}th,td{padding:11px 8px;border-bottom:1px solid var(--line);text-align:left;font-size:12px}th{color:var(--muted);font-weight:650}.notice{border-left:3px solid var(--amber);background:#fff8e9;padding:10px 12px;color:#76511c;font-size:12px}.notice.error{border-color:var(--red);background:#fff3f2;color:#892f2b}.toast{position:fixed;z-index:30;right:16px;bottom:calc(18px + env(safe-area-inset-bottom));left:16px;max-width:520px;margin:auto;border-radius:6px;background:var(--ink);padding:11px 14px;color:#fff;box-shadow:0 10px 30px rgba(23,33,29,.2);text-align:center}.toast.error{background:#7e2925}@media(min-width:761px){body{padding:22px}.shell{border:1px solid var(--line);border-radius:8px;overflow:hidden;box-shadow:0 18px 50px rgba(23,33,29,.08)}.workspace{min-height:calc(100vh - 106px)}.topbar{position:relative}.tabs{top:0}.view,.app-head{padding-right:28px;padding-left:28px}.toast{bottom:28px}}@media(max-width:420px){.balance strong{font-size:27px}.metrics{grid-template-columns:1fr}.metric{display:flex;align-items:center;justify-content:space-between;border-right:0;border-bottom:1px solid var(--line)}.metric:last-child{border-bottom:0}.metric strong{margin:0}.field-row{flex-direction:column}.field-row .button{width:100%}.topup{grid-template-columns:1fr}.topup .button{width:100%}}
|
|
1483
1602
|
</style></head><body>
|
|
1484
|
-
<
|
|
1485
|
-
<div
|
|
1486
|
-
<
|
|
1487
|
-
<
|
|
1488
|
-
<
|
|
1489
|
-
<
|
|
1603
|
+
<div class="shell"><header class="topbar"><div class="brand"><span class="mark">M</span><span>MycoMesh</span></div><div class="network"><span id="networkDot" class="dot"></span><span id="networkName">V8 Consumer</span></div></header><main class="workspace">
|
|
1604
|
+
<div id="locked" class="locked"><div class="locked-inner"><span class="locked-badge">Consumer V8</span><h1>连接钱包以继续</h1><p>签名后读取并核对当前 Key 的链上归属。</p><div class="key-preview"><span class="label">本地 Key 地址</span><span id="lockedKey" class="mono">读取中...</span></div><button id="login" class="button primary" type="button">连接钱包并签名</button><p id="loginError" class="notice error" hidden></p></div></div>
|
|
1605
|
+
<div id="app" hidden><div class="app-head"><div class="app-head-row"><div><span class="label">预付账户</span><h1>Consumer</h1></div><button id="walletButton" class="button small wallet-button" type="button">退出</button></div><div class="balance"><span class="label">可用余额</span><strong id="balance">--</strong><div class="balance-meta"><span id="keyStatus">Key 状态 --</span><span id="requestCount">0 次请求</span></div></div></div>
|
|
1606
|
+
<nav class="tabs" aria-label="Consumer navigation"><button class="tab active" data-view="overview" type="button">概览</button><button class="tab" data-view="wallet" type="button">钱包</button><button class="tab" data-view="activity" type="button">记录</button><button class="tab" data-view="share" type="button">分享</button></nav>
|
|
1607
|
+
<div id="view-overview" class="view"><div class="band"><div class="section-head"><div><h2>访问凭证</h2><p id="credentialState">等待 Key 激活</p></div><span id="credentialBadge" class="status">锁定</span></div><div id="credentials" hidden><div class="field"><span class="label">API URL</span><div class="field-row"><div id="url" class="value"></div><button class="button small copy" data-copy="url" type="button">复制</button></div></div><div class="field"><span class="label">Key</span><div class="field-row"><div id="key" class="value"></div><button class="button small copy" data-copy="key" type="button">复制</button></div></div><div class="field"><span class="label">Export</span><div class="field-row"><div id="export" class="value exports"></div><button class="button small copy" data-copy="export" type="button">复制</button></div></div></div><div id="inactiveKey" class="notice">当前 Key 尚未在链上激活。</div></div><div class="band"><div class="section-head"><h2>本地用量</h2></div><div class="metrics"><div class="metric"><span>累计消费</span><strong id="spent">--</strong></div><div class="metric"><span>输入 Tokens</span><strong id="inputTokens">0</strong></div><div class="metric"><span>输出 Tokens</span><strong id="outputTokens">0</strong></div></div></div></div>
|
|
1608
|
+
<div id="view-wallet" class="view" hidden><div class="band"><div class="section-head"><h2>钱包与 Key</h2><span id="chainStatus" class="status">读取中</span></div><dl class="list"><div><dt>钱包</dt><dd id="walletAddress" class="mono"></dd></div><div><dt>Key 地址</dt><dd id="keyAddress" class="mono"></dd></div><div><dt>单次上限</dt><dd id="keyLimit"></dd></div><div><dt>有效期</dt><dd id="keyValidity"></dd></div></dl><p id="chainError" class="notice error" hidden></p><div class="actions"><button id="activate" class="button primary" type="button">激活 Key</button><button id="rotate" class="button danger" type="button" hidden>更换 Key</button></div></div><div class="band"><div class="section-head"><div><h2>充值</h2><p id="walletBalance">钱包余额 --</p></div></div><div class="topup"><input id="amount" class="input" inputmode="decimal" placeholder="10.00 USDC"><button id="topup" class="button primary" type="button">充值</button></div></div></div>
|
|
1609
|
+
<div id="view-activity" class="view" hidden><div class="band"><div class="section-head"><div><h2>消费记录</h2><p>当前设备已确认的推理账单</p></div><button id="refresh" class="button small" type="button">刷新</button></div><div id="historyEmpty" class="empty">暂无消费记录</div><div id="historyTable" class="table-wrap" hidden><table><thead><tr><th>时间</th><th>模型</th><th>Tokens</th><th>费用</th><th>Provider</th></tr></thead><tbody id="history"></tbody></table></div></div></div>
|
|
1610
|
+
<div id="view-share" class="view" hidden><div class="band"><div class="section-head"><div><h2>临时分享</h2><p>到期后自动关闭</p></div><span id="shareStatus" class="status">未启用</span></div><div class="topup"><select id="shareMinutes" class="select"><option value="10">10 分钟</option><option value="30" selected>30 分钟</option><option value="60">1 小时</option><option value="360">6 小时</option></select><button id="shareStart" class="button primary" type="button">开始分享</button></div><div id="shareOutput" class="share-output" hidden><div class="field"><span class="label">API URL</span><div class="field-row"><div id="shareUrl" class="value"></div><button class="button small copy" data-copy="shareUrl" type="button">复制</button></div></div><div class="field"><span class="label">临时 Key</span><div class="field-row"><div id="shareKey" class="value"></div><button class="button small copy" data-copy="shareKey" type="button">复制</button></div></div><p id="shareExpiry" class="notice"></p><div class="actions"><button id="shareStop" class="button danger" type="button">停止分享</button></div></div></div></div></div>
|
|
1611
|
+
</main></div><div id="toast" class="toast" hidden></div>
|
|
1490
1612
|
<script>
|
|
1491
|
-
let state
|
|
1492
|
-
|
|
1493
|
-
function
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
}
|
|
1613
|
+
let state=null,wallet=null,managementToken=null,busy=false;const $=id=>document.getElementById(id);const short=value=>value?value.slice(0,6)+'...'+value.slice(-4):'--';
|
|
1614
|
+
function units(value,decimals=6){const raw=BigInt(value||0),base=10n**BigInt(decimals),whole=raw/base,fraction=(raw%base).toString().padStart(decimals,'0').replace(/0+$/,'');return whole.toLocaleString()+(fraction?'.'+fraction.slice(0,4):'')}
|
|
1615
|
+
function authHeaders(headers={}){const next=new Headers(headers);if(managementToken)next.set('authorization','Bearer '+managementToken);return next}
|
|
1616
|
+
async function api(path,options={}){const response=await fetch(path,{...options,headers:authHeaders(options.headers)}),data=await response.json();if(!response.ok)throw new Error(typeof data.error==='string'?data.error:(data.error?.message||'请求失败'));return data}
|
|
1617
|
+
function toast(message,error=false){const node=$('toast');node.textContent=message;node.className='toast'+(error?' error':'');node.hidden=false;clearTimeout(toast.timer);toast.timer=setTimeout(()=>node.hidden=true,3200)}
|
|
1618
|
+
function setBusy(value){busy=value;for(const button of document.querySelectorAll('button'))button.disabled=value}
|
|
1619
|
+
async function run(task){if(busy)return;setBusy(true);try{await task()}catch(error){toast(error?.message||String(error),true)}finally{setBusy(false)}}
|
|
1499
1620
|
async function load(){state=await api('/v1/mycomesh/local/dashboard');render()}
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
async function
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1621
|
+
function render(){const authenticated=Boolean(managementToken&&state.auth?.authenticated),ready=authenticated&&state.auth.key_ready,grant=state.key.grant||{},decimals=state.settlement?.stablecoin_decimals||6,symbol=state.settlement?.stablecoin_symbol||'USDC';$('networkName').textContent=state.chain_error?'链上不可用':(state.settlement?.network_name||'V8 Consumer');$('networkDot').className='dot'+(state.chain_error?'':' ok');$('lockedKey').textContent=state.key.address;$('locked').hidden=authenticated;$('app').hidden=!authenticated;if(!authenticated)return;$('walletButton').textContent=short(state.auth.wallet);$('walletAddress').textContent=state.auth.wallet;$('keyAddress').textContent=state.key.address;$('balance').textContent=state.account?units(state.account.available_balance_units,decimals)+' '+symbol:'--';$('requestCount').textContent=state.usage.request_count+' 次请求';$('keyStatus').textContent=ready?'Key 已激活':'Key 待激活';$('credentialBadge').className='status'+(ready?' ok':'');$('credentialBadge').textContent=ready?'可用':'待激活';$('credentialState').textContent=ready?'仅在本机显示':'链上确认后显示';$('credentials').hidden=!ready;$('inactiveKey').hidden=ready;if(ready){$('url').textContent=state.credentials.base_url;$('key').textContent=state.credentials.api_key;$('export').textContent=state.credentials.export}$('spent').textContent=units(state.usage.total_spent_units,decimals)+' '+symbol;$('inputTokens').textContent=Number(state.usage.input_tokens||0).toLocaleString();$('outputTokens').textContent=Number(state.usage.output_tokens||0).toLocaleString();$('keyLimit').textContent=grant.max_per_request?units(grant.max_per_request,decimals)+' '+symbol:'--';$('keyValidity').textContent=grant.valid_until?new Date(grant.valid_until*1000).toLocaleString():'长期有效';$('chainStatus').className='status'+(grant.active?' ok':'');$('chainStatus').textContent=grant.active?'链上有效':'等待激活';$('activate').hidden=ready;$('rotate').hidden=!ready;$('chainError').hidden=!state.chain_error;$('chainError').textContent=state.chain_error||'';$('walletBalance').textContent=state.wallet?'钱包余额 '+units(state.wallet.token_balance_units,decimals)+' '+symbol:'钱包余额 --';renderHistory();renderShare()}
|
|
1622
|
+
function renderHistory(){const body=$('history'),items=state.history||[];body.replaceChildren();$('historyEmpty').hidden=items.length>0;$('historyTable').hidden=items.length===0;for(const item of items){const row=document.createElement('tr');for(const value of [new Date(item.timestamp*1000).toLocaleString(),item.model,(item.input_tokens||0)+' / '+(item.output_tokens||0),units(item.actual_fee_units,state.settlement?.stablecoin_decimals||6),short(item.provider)]){const cell=document.createElement('td');cell.textContent=value;row.appendChild(cell)}body.appendChild(row)}}
|
|
1623
|
+
function renderShare(){const share=state.share||{};$('shareStatus').className='status'+(share.active?' ok':'');$('shareStatus').textContent=share.active?'分享中':'未启用';$('shareOutput').hidden=!share.active;if(share.active){$('shareUrl').textContent=share.base_url;$('shareKey').textContent=share.api_key;$('shareExpiry').textContent='到期时间 '+new Date(share.expires_at*1000).toLocaleString()}}
|
|
1624
|
+
function walletMessage(value){return '0x'+Array.from(new TextEncoder().encode(value),byte=>byte.toString(16).padStart(2,'0')).join('')}
|
|
1625
|
+
async function login(){if(!window.ethereum)throw new Error('未检测到浏览器钱包');const accounts=await window.ethereum.request({method:'eth_requestAccounts'});wallet=String(accounts[0]||'').toLowerCase();if(!wallet)throw new Error('钱包未连接');const challenge=await api('/v1/mycomesh/local/wallet/challenge',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({wallet})});const signature=await window.ethereum.request({method:'personal_sign',params:[walletMessage(challenge.message),wallet]});const result=await api('/v1/mycomesh/local/wallet/authenticate',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({wallet,signature})});managementToken=result.token;await load();toast(state.auth.key_ready?'钱包验证完成':'钱包验证完成,请激活 Key')}
|
|
1626
|
+
async function ensureChain(){const expected='0x'+Number(state.settlement.chain_id).toString(16),current=await window.ethereum.request({method:'eth_chainId'});if(current.toLowerCase()!==expected.toLowerCase())await window.ethereum.request({method:'wallet_switchEthereumChain',params:[{chainId:expected}]})}
|
|
1627
|
+
async function waitReceipt(hash){for(let count=0;count<120;count++){const receipt=await window.ethereum.request({method:'eth_getTransactionReceipt',params:[hash]});if(receipt){if(receipt.status!=='0x1')throw new Error('链上交易失败');return receipt}await new Promise(resolve=>setTimeout(resolve,1500))}throw new Error('等待链上确认超时')}
|
|
1628
|
+
async function sendPlan(plan){await ensureChain();for(const transaction of plan.transactions){toast(transaction.label);const hash=await window.ethereum.request({method:'eth_sendTransaction',params:[{from:wallet,to:transaction.to,data:transaction.data}]});await waitReceipt(hash)}}
|
|
1629
|
+
async function activateCurrent(){const plan=await api('/v1/mycomesh/local/transactions',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({action:'register_key',wallet})});await sendPlan(plan);for(let count=0;count<10;count++){try{await api('/v1/mycomesh/local/wallet/activate',{method:'POST'});await load();toast('Key 已激活');return}catch(error){if(count===9)throw error;await new Promise(resolve=>setTimeout(resolve,1600))}}}
|
|
1630
|
+
async function rotateKey(){const oldAddress=state.key.address;await api('/v1/mycomesh/local/key/prepare',{method:'POST'});const register=await api('/v1/mycomesh/local/transactions',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({action:'register_key',wallet})});await sendPlan(register);for(let count=0;count<10;count++){try{await api('/v1/mycomesh/local/key/activate',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({wallet})});break}catch(error){if(count===9)throw error;await new Promise(resolve=>setTimeout(resolve,1600))}}const revoke=await api('/v1/mycomesh/local/transactions',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({action:'revoke_key',wallet,key_address:oldAddress})});await sendPlan(revoke);await load();toast('新 Key 已启用,旧 Key 已撤销')}
|
|
1631
|
+
document.querySelectorAll('.tab').forEach(tab=>tab.addEventListener('click',()=>{document.querySelectorAll('.tab').forEach(item=>item.classList.toggle('active',item===tab));document.querySelectorAll('.view').forEach(view=>view.hidden=view.id!=='view-'+tab.dataset.view)}));
|
|
1632
|
+
document.addEventListener('click',event=>{const button=event.target.closest('.copy');if(button)navigator.clipboard.writeText($(button.dataset.copy).textContent).then(()=>toast('已复制')).catch(()=>toast('复制失败',true))});
|
|
1633
|
+
$('login').onclick=()=>run(login);$('activate').onclick=()=>run(activateCurrent);$('rotate').onclick=()=>run(rotateKey);$('refresh').onclick=()=>run(load);
|
|
1634
|
+
$('topup').onclick=()=>run(async()=>{const plan=await api('/v1/mycomesh/local/transactions',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({action:'top_up',wallet,amount_usdc:$('amount').value.trim()})});await sendPlan(plan);$('amount').value='';await load();toast('充值已确认')});
|
|
1635
|
+
$('shareStart').onclick=()=>run(async()=>{const value=await api('/v1/mycomesh/local/share/start',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({minutes:Number($('shareMinutes').value)})});state.share=value.share;renderShare();toast('临时分享已开启')});
|
|
1636
|
+
$('shareStop').onclick=()=>run(async()=>{const value=await api('/v1/mycomesh/local/share/stop',{method:'POST'});state.share=value.share;renderShare();toast('临时分享已停止')});
|
|
1637
|
+
$('walletButton').onclick=()=>run(async()=>{await api('/v1/mycomesh/local/wallet/lock',{method:'POST'});managementToken=null;wallet=null;await load()});
|
|
1638
|
+
window.ethereum?.on?.('accountsChanged',()=>{if(managementToken)api('/v1/mycomesh/local/wallet/lock',{method:'POST'}).catch(()=>{}).finally(()=>{managementToken=null;wallet=null;load().catch(error=>toast(error.message,true))})});
|
|
1639
|
+
load().catch(error=>{$('loginError').hidden=false;$('loginError').textContent=error.message});
|
|
1513
1640
|
</script></body></html>`;
|
|
1514
1641
|
}
|
|
1515
1642
|
|
package/src/consumer.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
createConsumerServer,
|
|
12
12
|
} from "./consumer-runtime.mjs";
|
|
13
13
|
|
|
14
|
-
export const CONSUMER_RELEASE_VERSION = "0.1.
|
|
14
|
+
export const CONSUMER_RELEASE_VERSION = "0.1.51";
|
|
15
15
|
export const API_COMMANDS = new Set(["health", "models", "responses", "chat"]);
|
|
16
16
|
const API_VALUE_OPTIONS = new Set([
|
|
17
17
|
"--base-url",
|
|
@@ -52,9 +52,9 @@ Options:
|
|
|
52
52
|
-h, --help Show this help
|
|
53
53
|
-v, --version Show the package version
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
Each Consumer process starts locked. Open the browser page, sign with the
|
|
56
|
+
wallet that owns the on-chain payment-key grant, then use the displayed export.
|
|
57
|
+
The page has no browser conversation state.`;
|
|
58
58
|
|
|
59
59
|
class ConsumerCliError extends Error {
|
|
60
60
|
constructor(message, exitCode = 1) {
|
|
@@ -105,7 +105,7 @@ export async function main(argv, dependencies = {}) {
|
|
|
105
105
|
const rootUrl = `${parsed.scheme}://${parsed.hostForUrl}:${parsed.port}`;
|
|
106
106
|
const credentialsUrl = `${rootUrl}/`;
|
|
107
107
|
writePid(parsed.dataDir);
|
|
108
|
-
stdout.write(`MycoMesh Consumer
|
|
108
|
+
stdout.write(`MycoMesh Consumer setup: ${credentialsUrl}\n`);
|
|
109
109
|
stdout.write(`OpenAI API: ${state.baseUrl}\n`);
|
|
110
110
|
if (!parsed.noBrowser) openBrowser(credentialsUrl, stderr);
|
|
111
111
|
|
|
@@ -119,6 +119,7 @@ export async function main(argv, dependencies = {}) {
|
|
|
119
119
|
return 0;
|
|
120
120
|
}
|
|
121
121
|
await waitUntilReady(state, parsed.readyTimeout, stdout);
|
|
122
|
+
await waitUntilUnlocked(state, parsed.readyTimeout, stdout);
|
|
122
123
|
const code = await runCodex(parsed, state, dependencies.spawn ?? defaultSpawn, stdout, stderr);
|
|
123
124
|
await shutdown();
|
|
124
125
|
return code;
|
|
@@ -221,6 +222,17 @@ async function waitUntilReady(state, timeoutSeconds, stdout) {
|
|
|
221
222
|
}
|
|
222
223
|
}
|
|
223
224
|
|
|
225
|
+
async function waitUntilUnlocked(state, timeoutSeconds, stdout) {
|
|
226
|
+
const started = Date.now();
|
|
227
|
+
stdout.write("Waiting for wallet verification in the local Consumer page...\n");
|
|
228
|
+
while (!state.paymentUnlocked) {
|
|
229
|
+
if (Date.now() - started >= timeoutSeconds * 1000) {
|
|
230
|
+
throw new ConsumerCliError("timed out waiting for the payment-key owner wallet to unlock Consumer");
|
|
231
|
+
}
|
|
232
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
224
236
|
function runCodex(parsed, state, spawn, stdout, stderr) {
|
|
225
237
|
return new Promise((resolve) => {
|
|
226
238
|
const args = [
|