mycomesh-consumer 0.1.49 → 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 +191 -57
- 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);
|
|
@@ -680,7 +768,7 @@ export class NativeConsumerState {
|
|
|
680
768
|
}).filter((value) => value && typeof value === "object").reverse();
|
|
681
769
|
}
|
|
682
770
|
|
|
683
|
-
recordReceipt(relayUrl, endpoint, model, settlement) {
|
|
771
|
+
recordReceipt(relayUrl, endpoint, model, settlement, routeModel = model) {
|
|
684
772
|
const signed = settlement?.signed_receipt;
|
|
685
773
|
const receipt = signed?.receipt;
|
|
686
774
|
const auth = signed?.authorization?.authorization;
|
|
@@ -693,6 +781,7 @@ export class NativeConsumerState {
|
|
|
693
781
|
accepted: Boolean(settlement.accepted),
|
|
694
782
|
endpoint,
|
|
695
783
|
model,
|
|
784
|
+
route_model: routeModel,
|
|
696
785
|
relay_url: relayUrl,
|
|
697
786
|
provider: String(receipt.provider || ""),
|
|
698
787
|
input_tokens: Number(receipt.input_tokens || 0),
|
|
@@ -745,36 +834,45 @@ export class NativeConsumerState {
|
|
|
745
834
|
return BigInt(output || "0x0").toString();
|
|
746
835
|
}
|
|
747
836
|
|
|
748
|
-
async dashboardPayload(
|
|
837
|
+
async dashboardPayload(managementAuthorized = false) {
|
|
838
|
+
const authenticated = managementAuthorized && Boolean(this.unlockedWallet);
|
|
749
839
|
const allHistory = this.history(0);
|
|
840
|
+
const pending = authenticated ? this.pendingPaymentKey() : null;
|
|
750
841
|
const payload = {
|
|
751
842
|
ok: true,
|
|
752
843
|
protocol_version: 8,
|
|
753
844
|
runtime: "node-native",
|
|
754
|
-
|
|
755
|
-
|
|
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
|
+
},
|
|
756
854
|
settlement: this.network,
|
|
757
|
-
history: allHistory.slice(0, 100),
|
|
855
|
+
history: authenticated ? allHistory.slice(0, 100) : [],
|
|
758
856
|
usage: {
|
|
759
|
-
request_count: allHistory.length,
|
|
760
|
-
total_spent_units: allHistory.reduce((total, item) => total + Number(item.actual_fee_units || 0), 0),
|
|
761
|
-
input_tokens: allHistory.reduce((total, item) => total + Number(item.input_tokens || 0), 0),
|
|
762
|
-
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,
|
|
763
861
|
},
|
|
764
|
-
share: this.sharePayload(),
|
|
862
|
+
share: authenticated ? this.sharePayload() : { active: false },
|
|
765
863
|
};
|
|
766
864
|
try {
|
|
767
865
|
const grant = await this.keyGrant(this.paymentAddress);
|
|
768
866
|
payload.key.grant = grant;
|
|
769
|
-
if (grant.owner !== ZERO_ADDRESS) {
|
|
867
|
+
if (authenticated && grant.owner !== ZERO_ADDRESS) {
|
|
770
868
|
payload.account = { owner: grant.owner, available_balance_units: await this.accountBalance(grant.owner) };
|
|
771
869
|
}
|
|
772
870
|
} catch (error) {
|
|
773
871
|
payload.chain_error = error.message;
|
|
774
872
|
}
|
|
775
|
-
if (
|
|
873
|
+
if (authenticated) {
|
|
776
874
|
try {
|
|
777
|
-
const address =
|
|
875
|
+
const address = this.unlockedWallet;
|
|
778
876
|
const token = await this.rpcValue((rpc) => this.contractCall(rpc, this.network.stablecoin, "balanceOf(address)", [address]));
|
|
779
877
|
const allowance = await this.rpcValue((rpc) => this.contractCall(rpc, this.network.stablecoin, "allowance(address,address)", [address, this.network.settlement_contract]));
|
|
780
878
|
payload.wallet = { address, token_balance_units: BigInt(token || "0x0").toString(), allowance_units: BigInt(allowance || "0x0").toString() };
|
|
@@ -804,12 +902,13 @@ export class NativeConsumerState {
|
|
|
804
902
|
}
|
|
805
903
|
|
|
806
904
|
authorizeBearer(authorization, { shareOnly = false } = {}) {
|
|
807
|
-
if (!shareOnly && sameSecret(authorization, `Bearer ${this.paymentKey}`)) return true;
|
|
905
|
+
if (!shareOnly && this.paymentUnlocked && sameSecret(authorization, `Bearer ${this.paymentKey}`)) return true;
|
|
808
906
|
const share = this.activeShare();
|
|
809
907
|
return Boolean(share && sameSecret(authorization, `Bearer ${share.apiKey}`));
|
|
810
908
|
}
|
|
811
909
|
|
|
812
910
|
async startShare(minutesValue) {
|
|
911
|
+
if (!this.paymentUnlocked) throw new Error("sign in and activate the payment key before sharing");
|
|
813
912
|
const minutes = Number(minutesValue);
|
|
814
913
|
if (!Number.isInteger(minutes) || minutes < 1 || minutes > MAX_SHARE_MINUTES) {
|
|
815
914
|
throw new Error(`share duration must be between 1 and ${MAX_SHARE_MINUTES} minutes`);
|
|
@@ -888,7 +987,7 @@ export class NativeConsumerState {
|
|
|
888
987
|
}
|
|
889
988
|
|
|
890
989
|
async activatePendingPaymentKey(wallet) {
|
|
891
|
-
const owner =
|
|
990
|
+
const owner = this.assertUnlockedWallet(wallet);
|
|
892
991
|
const pending = this.pendingPaymentKey();
|
|
893
992
|
if (!pending) throw new Error("no pending payment key exists");
|
|
894
993
|
const grant = await this.keyGrant(pending.payment_key_address);
|
|
@@ -900,12 +999,13 @@ export class NativeConsumerState {
|
|
|
900
999
|
unlinkSync(this.pendingKeyPath);
|
|
901
1000
|
this.paymentKey = pending.payment_key;
|
|
902
1001
|
this.paymentAddress = pending.payment_key_address;
|
|
903
|
-
|
|
1002
|
+
this.paymentUnlocked = true;
|
|
1003
|
+
return { payment_key_address: this.paymentAddress, previous_key_address: previous };
|
|
904
1004
|
}
|
|
905
1005
|
|
|
906
1006
|
async transactionPlan(raw) {
|
|
907
1007
|
const action = String(raw?.action || "");
|
|
908
|
-
const wallet =
|
|
1008
|
+
const wallet = this.assertUnlockedWallet(raw?.wallet);
|
|
909
1009
|
const settlement = this.network.settlement_contract;
|
|
910
1010
|
const token = this.network.stablecoin;
|
|
911
1011
|
if (action === "top_up") {
|
|
@@ -1048,7 +1148,13 @@ export class NativeConsumerState {
|
|
|
1048
1148
|
const paymentResponse = response.headers.get("PAYMENT-RESPONSE");
|
|
1049
1149
|
if (paymentResponse) {
|
|
1050
1150
|
const settlement = decodePaymentResponse(paymentResponse);
|
|
1051
|
-
this.recordReceipt(
|
|
1151
|
+
this.recordReceipt(
|
|
1152
|
+
selected.relayUrl,
|
|
1153
|
+
path,
|
|
1154
|
+
String(body.model || payment.model),
|
|
1155
|
+
settlement,
|
|
1156
|
+
payment.model,
|
|
1157
|
+
);
|
|
1052
1158
|
headers["PAYMENT-RESPONSE"] = paymentResponse;
|
|
1053
1159
|
}
|
|
1054
1160
|
return { payload: restoreClientResponse(payload, body), status: 200, headers };
|
|
@@ -1308,6 +1414,7 @@ export function createConsumerServer(state, { host = "127.0.0.1", port = 8110, p
|
|
|
1308
1414
|
return;
|
|
1309
1415
|
}
|
|
1310
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; }
|
|
1311
1418
|
response.writeHead(200, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" }); response.end(`${state.credentialsText()}\n`); return;
|
|
1312
1419
|
}
|
|
1313
1420
|
if (request.method === "GET" && (path === "/models" || path === "/v1/models" || path === "/backend-api/codex/models")) {
|
|
@@ -1315,31 +1422,54 @@ export function createConsumerServer(state, { host = "127.0.0.1", port = 8110, p
|
|
|
1315
1422
|
catch (error) { writeJson(response, 503, openaiError(error.message, "relay_unavailable")); }
|
|
1316
1423
|
return;
|
|
1317
1424
|
}
|
|
1318
|
-
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
|
+
}
|
|
1319
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; }
|
|
1320
1450
|
try { writeJson(response, 200, await state.transactionPlan(await decodeRequestBody(request))); }
|
|
1321
1451
|
catch (error) { writeJson(response, 400, { ok: false, error: error.message }); }
|
|
1322
1452
|
return;
|
|
1323
1453
|
}
|
|
1324
1454
|
if (request.method === "POST" && path === "/v1/mycomesh/local/key/prepare") {
|
|
1325
|
-
if (!
|
|
1455
|
+
if (!state.authorizeManagement(String(request.headers.authorization || ""))) { writeJson(response, 401, { ok: false, error: "wallet login required" }); return; }
|
|
1326
1456
|
try { writeJson(response, 200, state.preparePaymentKey()); } catch (error) { writeJson(response, 400, { ok: false, error: error.message }); }
|
|
1327
1457
|
return;
|
|
1328
1458
|
}
|
|
1329
1459
|
if (request.method === "POST" && path === "/v1/mycomesh/local/key/activate") {
|
|
1330
|
-
if (!
|
|
1460
|
+
if (!state.authorizeManagement(String(request.headers.authorization || ""))) { writeJson(response, 401, { ok: false, error: "wallet login required" }); return; }
|
|
1331
1461
|
try { const value = await decodeRequestBody(request); writeJson(response, 200, await state.activatePendingPaymentKey(value.wallet)); }
|
|
1332
1462
|
catch (error) { writeJson(response, 400, { ok: false, error: error.message }); }
|
|
1333
1463
|
return;
|
|
1334
1464
|
}
|
|
1335
1465
|
if (request.method === "POST" && path === "/v1/mycomesh/local/share/start") {
|
|
1336
|
-
if (!
|
|
1466
|
+
if (!state.authorizeManagement(String(request.headers.authorization || ""))) { writeJson(response, 401, { ok: false, error: "wallet login required" }); return; }
|
|
1337
1467
|
try { const value = await decodeRequestBody(request); writeJson(response, 200, { ok: true, share: await state.startShare(value.minutes) }); }
|
|
1338
1468
|
catch (error) { writeJson(response, 400, { ok: false, error: error.message }); }
|
|
1339
1469
|
return;
|
|
1340
1470
|
}
|
|
1341
1471
|
if (request.method === "POST" && path === "/v1/mycomesh/local/share/stop") {
|
|
1342
|
-
if (!
|
|
1472
|
+
if (!state.authorizeManagement(String(request.headers.authorization || ""))) { writeJson(response, 401, { ok: false, error: "wallet login required" }); return; }
|
|
1343
1473
|
writeJson(response, 200, { ok: true, share: await state.stopShare() });
|
|
1344
1474
|
return;
|
|
1345
1475
|
}
|
|
@@ -1465,44 +1595,48 @@ export function chatCompletionSse(payload, includeUsage = false) {
|
|
|
1465
1595
|
|
|
1466
1596
|
function consumerHtml() {
|
|
1467
1597
|
return `<!doctype html>
|
|
1468
|
-
<html lang="
|
|
1469
|
-
<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>
|
|
1470
1600
|
<style>
|
|
1471
|
-
body{font:
|
|
1472
|
-
h1{margin-bottom:4px}.muted{color:#68736e}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:14px}
|
|
1473
|
-
.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}
|
|
1474
|
-
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}
|
|
1475
|
-
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%}}
|
|
1476
1602
|
</style></head><body>
|
|
1477
|
-
<
|
|
1478
|
-
<div
|
|
1479
|
-
<
|
|
1480
|
-
<
|
|
1481
|
-
<
|
|
1482
|
-
<
|
|
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>
|
|
1483
1612
|
<script>
|
|
1484
|
-
let state
|
|
1485
|
-
|
|
1486
|
-
function
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
}
|
|
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)}}
|
|
1492
1620
|
async function load(){state=await api('/v1/mycomesh/local/dashboard');render()}
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
async function
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
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});
|
|
1506
1640
|
</script></body></html>`;
|
|
1507
1641
|
}
|
|
1508
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 = [
|