@bolloon/bolloon-agent 0.4.29 → 0.4.30
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/dist/agents/contacts/chain.js +579 -0
- package/dist/agents/contacts/cli.js +158 -0
- package/dist/agents/contacts/consent.js +124 -0
- package/dist/agents/contacts/grant-payload.js +45 -0
- package/dist/agents/contacts/grants.js +451 -0
- package/dist/agents/contacts/policy.js +264 -0
- package/dist/agents/contacts/preview-types.js +6 -0
- package/dist/agents/contacts/providers.js +341 -0
- package/dist/agents/contacts/store.js +326 -0
- package/dist/agents/contacts/tools.js +154 -0
- package/dist/agents/contacts/types.js +148 -0
- package/dist/agents/pi-sdk.js +10 -0
- package/dist/electron-build/agents/contacts/chain.js +619 -0
- package/dist/electron-build/agents/contacts/chain.js.map +1 -0
- package/dist/electron-build/agents/contacts/consent.js +163 -0
- package/dist/electron-build/agents/contacts/consent.js.map +1 -0
- package/dist/electron-build/agents/contacts/grant-payload.js +52 -0
- package/dist/electron-build/agents/contacts/grant-payload.js.map +1 -0
- package/dist/electron-build/agents/contacts/grants.js +500 -0
- package/dist/electron-build/agents/contacts/grants.js.map +1 -0
- package/dist/electron-build/agents/contacts/policy.js +271 -0
- package/dist/electron-build/agents/contacts/policy.js.map +1 -0
- package/dist/electron-build/agents/contacts/preview-types.js +8 -0
- package/dist/electron-build/agents/contacts/preview-types.js.map +1 -0
- package/dist/electron-build/agents/contacts/providers.js +384 -0
- package/dist/electron-build/agents/contacts/providers.js.map +1 -0
- package/dist/electron-build/agents/contacts/store.js +370 -0
- package/dist/electron-build/agents/contacts/store.js.map +1 -0
- package/dist/electron-build/agents/contacts/tools.js +159 -0
- package/dist/electron-build/agents/contacts/tools.js.map +1 -0
- package/dist/electron-build/agents/contacts/types.js +166 -0
- package/dist/electron-build/agents/contacts/types.js.map +1 -0
- package/dist/electron-build/agents/external-events.js.map +1 -1
- package/dist/electron-build/agents/goal-store.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk.js +10 -0
- package/dist/electron-build/agents/pi-sdk.js.map +1 -1
- package/dist/electron-build/security/tool-gate.js +4 -0
- package/dist/electron-build/security/tool-gate.js.map +1 -1
- package/dist/index.js +30 -0
- package/dist/security/tool-gate.js +4 -0
- package/dist/web/mobile-contacts.js +329 -0
- package/dist/web/mobile-core.js +481 -15
- package/dist/web/mobile.html +32 -0
- package/dist/web/mobile.js +139 -0
- package/dist/web/routes-contacts.js +420 -0
- package/dist/web/server.js +5 -0
- package/package.json +1 -1
package/dist/web/mobile-core.js
CHANGED
|
@@ -71270,7 +71270,7 @@ ${[...listenStats.errors.entries()].map(([addr, err2]) => {
|
|
|
71270
71270
|
i = this._head + i & this._capacityMask;
|
|
71271
71271
|
return this._list[i];
|
|
71272
71272
|
};
|
|
71273
|
-
Denque2.prototype.get = function
|
|
71273
|
+
Denque2.prototype.get = function get3(i) {
|
|
71274
71274
|
return this.peekAt(i);
|
|
71275
71275
|
};
|
|
71276
71276
|
Denque2.prototype.peek = function peek() {
|
|
@@ -103541,7 +103541,7 @@ ${r.steps.map((s3) => `${s3.ok ? "\u2713" : "\u2717"} ${s3.step}: ${s3.note}`).j
|
|
|
103541
103541
|
}
|
|
103542
103542
|
}
|
|
103543
103543
|
}
|
|
103544
|
-
function
|
|
103544
|
+
function get2(source, path) {
|
|
103545
103545
|
let node2 = source;
|
|
103546
103546
|
for (const [index, key] of path.entries()) {
|
|
103547
103547
|
node2 = node2[key];
|
|
@@ -103597,7 +103597,7 @@ ${r.steps.map((s3) => `${s3.ok ? "\u2713" : "\u2717"} ${s3.step}: ${s3.note}`).j
|
|
|
103597
103597
|
return tree(this.value, []);
|
|
103598
103598
|
}
|
|
103599
103599
|
get(path = "/") {
|
|
103600
|
-
return
|
|
103600
|
+
return get2(this.value, path.split("/").filter(Boolean));
|
|
103601
103601
|
}
|
|
103602
103602
|
};
|
|
103603
103603
|
}
|
|
@@ -119947,46 +119947,46 @@ ${values.join("\n")}` : `${blockName} :`;
|
|
|
119947
119947
|
var len;
|
|
119948
119948
|
revLookup["-".charCodeAt(0)] = 62;
|
|
119949
119949
|
revLookup["_".charCodeAt(0)] = 63;
|
|
119950
|
-
function getLens(
|
|
119951
|
-
var len2 =
|
|
119950
|
+
function getLens(b642) {
|
|
119951
|
+
var len2 = b642.length;
|
|
119952
119952
|
if (len2 % 4 > 0) {
|
|
119953
119953
|
throw new Error("Invalid string. Length must be a multiple of 4");
|
|
119954
119954
|
}
|
|
119955
|
-
var validLen =
|
|
119955
|
+
var validLen = b642.indexOf("=");
|
|
119956
119956
|
if (validLen === -1) validLen = len2;
|
|
119957
119957
|
var placeHoldersLen = validLen === len2 ? 0 : 4 - validLen % 4;
|
|
119958
119958
|
return [validLen, placeHoldersLen];
|
|
119959
119959
|
}
|
|
119960
|
-
function byteLength2(
|
|
119961
|
-
var lens = getLens(
|
|
119960
|
+
function byteLength2(b642) {
|
|
119961
|
+
var lens = getLens(b642);
|
|
119962
119962
|
var validLen = lens[0];
|
|
119963
119963
|
var placeHoldersLen = lens[1];
|
|
119964
119964
|
return (validLen + placeHoldersLen) * 3 / 4 - placeHoldersLen;
|
|
119965
119965
|
}
|
|
119966
|
-
function _byteLength(
|
|
119966
|
+
function _byteLength(b642, validLen, placeHoldersLen) {
|
|
119967
119967
|
return (validLen + placeHoldersLen) * 3 / 4 - placeHoldersLen;
|
|
119968
119968
|
}
|
|
119969
|
-
function toByteArray(
|
|
119969
|
+
function toByteArray(b642) {
|
|
119970
119970
|
var tmp;
|
|
119971
|
-
var lens = getLens(
|
|
119971
|
+
var lens = getLens(b642);
|
|
119972
119972
|
var validLen = lens[0];
|
|
119973
119973
|
var placeHoldersLen = lens[1];
|
|
119974
|
-
var arr = new Arr(_byteLength(
|
|
119974
|
+
var arr = new Arr(_byteLength(b642, validLen, placeHoldersLen));
|
|
119975
119975
|
var curByte = 0;
|
|
119976
119976
|
var len2 = placeHoldersLen > 0 ? validLen - 4 : validLen;
|
|
119977
119977
|
var i2;
|
|
119978
119978
|
for (i2 = 0; i2 < len2; i2 += 4) {
|
|
119979
|
-
tmp = revLookup[
|
|
119979
|
+
tmp = revLookup[b642.charCodeAt(i2)] << 18 | revLookup[b642.charCodeAt(i2 + 1)] << 12 | revLookup[b642.charCodeAt(i2 + 2)] << 6 | revLookup[b642.charCodeAt(i2 + 3)];
|
|
119980
119980
|
arr[curByte++] = tmp >> 16 & 255;
|
|
119981
119981
|
arr[curByte++] = tmp >> 8 & 255;
|
|
119982
119982
|
arr[curByte++] = tmp & 255;
|
|
119983
119983
|
}
|
|
119984
119984
|
if (placeHoldersLen === 2) {
|
|
119985
|
-
tmp = revLookup[
|
|
119985
|
+
tmp = revLookup[b642.charCodeAt(i2)] << 2 | revLookup[b642.charCodeAt(i2 + 1)] >> 4;
|
|
119986
119986
|
arr[curByte++] = tmp & 255;
|
|
119987
119987
|
}
|
|
119988
119988
|
if (placeHoldersLen === 1) {
|
|
119989
|
-
tmp = revLookup[
|
|
119989
|
+
tmp = revLookup[b642.charCodeAt(i2)] << 10 | revLookup[b642.charCodeAt(i2 + 1)] << 4 | revLookup[b642.charCodeAt(i2 + 2)] >> 2;
|
|
119990
119990
|
arr[curByte++] = tmp >> 8 & 255;
|
|
119991
119991
|
arr[curByte++] = tmp & 255;
|
|
119992
119992
|
}
|
|
@@ -162923,6 +162923,365 @@ ${error.stack}` : head;
|
|
|
162923
162923
|
return result;
|
|
162924
162924
|
}
|
|
162925
162925
|
|
|
162926
|
+
// src/agents/contacts/grant-payload.ts
|
|
162927
|
+
function canonicalGrantPayload(g) {
|
|
162928
|
+
return JSON.stringify({
|
|
162929
|
+
grantId: g.grantId,
|
|
162930
|
+
identityId: g.identityId,
|
|
162931
|
+
ownerDid: g.ownerDid,
|
|
162932
|
+
level: g.level,
|
|
162933
|
+
channels: g.channels,
|
|
162934
|
+
contactScope: g.contactScope,
|
|
162935
|
+
taskScope: g.taskScope,
|
|
162936
|
+
contentScope: g.contentScope,
|
|
162937
|
+
replyWakeAllowed: g.replyWakeAllowed,
|
|
162938
|
+
autoSend: g.autoSend,
|
|
162939
|
+
sensitiveContentAllowed: g.sensitiveContentAllowed,
|
|
162940
|
+
newRecipientAllowed: g.newRecipientAllowed,
|
|
162941
|
+
grantedAt: g.grantedAt,
|
|
162942
|
+
grantedBy: g.grantedBy,
|
|
162943
|
+
grantedVia: g.grantedVia,
|
|
162944
|
+
deviceIds: [...g.deviceIds].sort(),
|
|
162945
|
+
status: g.status,
|
|
162946
|
+
revokedAt: g.revokedAt || "",
|
|
162947
|
+
grantVersion: g.grantVersion
|
|
162948
|
+
});
|
|
162949
|
+
}
|
|
162950
|
+
function canonicalRevocationPayload(r) {
|
|
162951
|
+
return JSON.stringify({ grantId: r.grantId, grantVersion: r.grantVersion, revokedAt: r.revokedAt, by: r.by });
|
|
162952
|
+
}
|
|
162953
|
+
function presetForChoice(choice) {
|
|
162954
|
+
const base6 = { channels: "both", replyWakeAllowed: true, autoSend: true, newRecipientAllowed: false };
|
|
162955
|
+
switch (choice) {
|
|
162956
|
+
case "task_once":
|
|
162957
|
+
return { ...base6, level: "task_once", contactScope: "verified_contacts", taskScope: "current_goal", contentScope: "normal", autoSend: false, sensitiveContentAllowed: false };
|
|
162958
|
+
case "persistent":
|
|
162959
|
+
return { ...base6, level: "persistent", contactScope: "verified_contacts", taskScope: "all_future_goals", contentScope: "normal", sensitiveContentAllowed: false };
|
|
162960
|
+
case "full_contact_access":
|
|
162961
|
+
return { ...base6, level: "full_contact_access", contactScope: "all_contacts", taskScope: "all_future_goals", contentScope: "sensitive", sensitiveContentAllowed: true };
|
|
162962
|
+
}
|
|
162963
|
+
}
|
|
162964
|
+
|
|
162965
|
+
// src/web/mobile-contacts.ts
|
|
162966
|
+
var K_DEVICE = "bolloon.contacts.device.v1";
|
|
162967
|
+
var K_QUEUE = "bolloon.contacts.queue.v1";
|
|
162968
|
+
var K_CAPS = "bolloon.contacts.capabilities.v1";
|
|
162969
|
+
var cryptoOf = (c) => c || globalThis.crypto || null;
|
|
162970
|
+
function ed25519Available(c) {
|
|
162971
|
+
const cobj = cryptoOf(c);
|
|
162972
|
+
if (!cobj?.subtle) return false;
|
|
162973
|
+
try {
|
|
162974
|
+
const alg = { name: "Ed25519" };
|
|
162975
|
+
return typeof cobj.subtle.importKey === "function" && !!alg;
|
|
162976
|
+
} catch {
|
|
162977
|
+
return false;
|
|
162978
|
+
}
|
|
162979
|
+
}
|
|
162980
|
+
function bytesOf(s2) {
|
|
162981
|
+
const src4 = new TextEncoder().encode(s2);
|
|
162982
|
+
const out = new Uint8Array(new ArrayBuffer(src4.byteLength));
|
|
162983
|
+
out.set(src4);
|
|
162984
|
+
return out;
|
|
162985
|
+
}
|
|
162986
|
+
function b64(buf) {
|
|
162987
|
+
const arr = new Uint8Array(buf);
|
|
162988
|
+
let out = "";
|
|
162989
|
+
for (let i = 0; i < arr.length; i++) out += String.fromCharCode(arr[i]);
|
|
162990
|
+
return btoa(out);
|
|
162991
|
+
}
|
|
162992
|
+
function pemFromSpki(spki) {
|
|
162993
|
+
const b = b64(spki).replace(/(.{64})/g, "$1\n").trim();
|
|
162994
|
+
return `-----BEGIN PUBLIC KEY-----
|
|
162995
|
+
${b}
|
|
162996
|
+
-----END PUBLIC KEY-----
|
|
162997
|
+
`;
|
|
162998
|
+
}
|
|
162999
|
+
async function sha256Hex(c, text) {
|
|
163000
|
+
const d2 = await c.subtle.digest("SHA-256", bytesOf(text));
|
|
163001
|
+
return [...new Uint8Array(d2)].map((x) => x.toString(16).padStart(2, "0")).join("");
|
|
163002
|
+
}
|
|
163003
|
+
async function loadOrCreateDeviceKey(storage2, c) {
|
|
163004
|
+
const cobj = cryptoOf(c);
|
|
163005
|
+
if (!cobj?.subtle) throw new Error("device_signing_unavailable: \u672C\u673A\u6CA1\u6709 WebCrypto");
|
|
163006
|
+
const raw = storage2.getItem(K_DEVICE);
|
|
163007
|
+
if (raw) {
|
|
163008
|
+
try {
|
|
163009
|
+
const parsed = JSON.parse(raw);
|
|
163010
|
+
if (parsed?.privateKeyJwk && parsed?.publicKeyPem && parsed?.deviceId) return parsed;
|
|
163011
|
+
} catch {
|
|
163012
|
+
}
|
|
163013
|
+
}
|
|
163014
|
+
let pair;
|
|
163015
|
+
try {
|
|
163016
|
+
pair = await cobj.subtle.generateKey({ name: "Ed25519" }, true, ["sign", "verify"]);
|
|
163017
|
+
} catch (err2) {
|
|
163018
|
+
throw new Error(`device_signing_unavailable: ${String(err2?.message || err2)}`);
|
|
163019
|
+
}
|
|
163020
|
+
const publicKeyPem = pemFromSpki(await cobj.subtle.exportKey("spki", pair.publicKey));
|
|
163021
|
+
const privateKeyJwk = await cobj.subtle.exportKey("jwk", pair.privateKey);
|
|
163022
|
+
const deviceId = `dev-${(await sha256Hex(cobj, publicKeyPem)).slice(0, 12)}`;
|
|
163023
|
+
const material = { deviceId, publicKeyPem, privateKeyJwk };
|
|
163024
|
+
storage2.setItem(K_DEVICE, JSON.stringify(material));
|
|
163025
|
+
return material;
|
|
163026
|
+
}
|
|
163027
|
+
async function importPrivate(storage2, c) {
|
|
163028
|
+
const k = await loadOrCreateDeviceKey(storage2, c);
|
|
163029
|
+
return c.subtle.importKey("jwk", k.privateKeyJwk, { name: "Ed25519" }, false, ["sign"]);
|
|
163030
|
+
}
|
|
163031
|
+
async function signGrantOnDevice(grant, storage2, c) {
|
|
163032
|
+
const cobj = cryptoOf(c);
|
|
163033
|
+
const key = await importPrivate(storage2, cobj);
|
|
163034
|
+
const payload = canonicalGrantPayload(grant);
|
|
163035
|
+
const sig = await cobj.subtle.sign({ name: "Ed25519" }, key, bytesOf(payload));
|
|
163036
|
+
const dev = await loadOrCreateDeviceKey(storage2, cobj);
|
|
163037
|
+
return { deviceId: dev.deviceId, alg: "ed25519", payloadHash: await sha256Hex(cobj, payload), sig: b64(sig) };
|
|
163038
|
+
}
|
|
163039
|
+
async function signRevocationOnDevice(rev, storage2, c) {
|
|
163040
|
+
const cobj = cryptoOf(c);
|
|
163041
|
+
const key = await importPrivate(storage2, cobj);
|
|
163042
|
+
const payload = canonicalRevocationPayload(rev);
|
|
163043
|
+
const sig = await cobj.subtle.sign({ name: "Ed25519" }, key, bytesOf(payload));
|
|
163044
|
+
const dev = await loadOrCreateDeviceKey(storage2, cobj);
|
|
163045
|
+
return { deviceId: dev.deviceId, alg: "ed25519", payloadHash: await sha256Hex(cobj, payload), sig: b64(sig) };
|
|
163046
|
+
}
|
|
163047
|
+
async function post(base6, path, body, f) {
|
|
163048
|
+
const res = await f(`${String(base6).replace(/\/+$/, "")}${path}`, {
|
|
163049
|
+
method: "POST",
|
|
163050
|
+
headers: { "Content-Type": "application/json" },
|
|
163051
|
+
body: JSON.stringify(body ?? {})
|
|
163052
|
+
});
|
|
163053
|
+
let json = null;
|
|
163054
|
+
try {
|
|
163055
|
+
json = await res.json();
|
|
163056
|
+
} catch {
|
|
163057
|
+
}
|
|
163058
|
+
return { status: res.status, json };
|
|
163059
|
+
}
|
|
163060
|
+
async function get(base6, path, f) {
|
|
163061
|
+
const res = await f(`${String(base6).replace(/\/+$/, "")}${path}`);
|
|
163062
|
+
let json = null;
|
|
163063
|
+
try {
|
|
163064
|
+
json = await res.json();
|
|
163065
|
+
} catch {
|
|
163066
|
+
}
|
|
163067
|
+
return { status: res.status, json };
|
|
163068
|
+
}
|
|
163069
|
+
async function registerDeviceOnDesktop(base6, storage2, opts = {}) {
|
|
163070
|
+
const f = opts.fetchImpl || fetch;
|
|
163071
|
+
const dev = await loadOrCreateDeviceKey(storage2, opts.cryptoObj);
|
|
163072
|
+
const r = await post(base6, "/api/contacts/devices", { deviceId: dev.deviceId, publicKeyPem: dev.publicKeyPem, label: opts.label || "\u624B\u673A" }, f);
|
|
163073
|
+
if (r.status !== 200 || !r.json?.ok) return { ok: false, error: `\u684C\u9762\u672A\u63A5\u53D7\u8BBE\u5907\u767B\u8BB0: ${r.json?.error || r.status}`, data: dev };
|
|
163074
|
+
return { ok: true, data: dev };
|
|
163075
|
+
}
|
|
163076
|
+
function saveCapabilityCopy(storage2, items) {
|
|
163077
|
+
const clean8 = (items || []).map((c) => ({
|
|
163078
|
+
contactId: String(c.contactId || ""),
|
|
163079
|
+
kind: c.kind,
|
|
163080
|
+
displayValue: String(c.displayValue || ""),
|
|
163081
|
+
verificationStatus: String(c.verificationStatus || ""),
|
|
163082
|
+
capabilities: [...c.capabilities || []],
|
|
163083
|
+
provider: c.provider ? String(c.provider) : void 0,
|
|
163084
|
+
state: c.state ? String(c.state) : void 0
|
|
163085
|
+
}));
|
|
163086
|
+
storage2.setItem(K_CAPS, JSON.stringify(clean8));
|
|
163087
|
+
}
|
|
163088
|
+
function loadCapabilityCopy(storage2) {
|
|
163089
|
+
try {
|
|
163090
|
+
const v = JSON.parse(storage2.getItem(K_CAPS) || "[]");
|
|
163091
|
+
return Array.isArray(v) ? v : [];
|
|
163092
|
+
} catch {
|
|
163093
|
+
return [];
|
|
163094
|
+
}
|
|
163095
|
+
}
|
|
163096
|
+
function pendingQueue(storage2) {
|
|
163097
|
+
try {
|
|
163098
|
+
const v = JSON.parse(storage2.getItem(K_QUEUE) || "[]");
|
|
163099
|
+
return Array.isArray(v) ? v : [];
|
|
163100
|
+
} catch {
|
|
163101
|
+
return [];
|
|
163102
|
+
}
|
|
163103
|
+
}
|
|
163104
|
+
function setQueue(storage2, q) {
|
|
163105
|
+
storage2.setItem(K_QUEUE, JSON.stringify(q.slice(-20)));
|
|
163106
|
+
}
|
|
163107
|
+
async function flushQueuedGrants(base6, storage2, opts = {}) {
|
|
163108
|
+
const f = opts.fetchImpl || fetch;
|
|
163109
|
+
const q = pendingQueue(storage2);
|
|
163110
|
+
const keep = [];
|
|
163111
|
+
const results = [];
|
|
163112
|
+
let sent = 0, failed = 0;
|
|
163113
|
+
for (const item of q) {
|
|
163114
|
+
try {
|
|
163115
|
+
const r = await post(base6, "/api/contacts/grants/sync", { grant: { ...item.grant, signature: item.signature } }, f);
|
|
163116
|
+
if (r.status === 200 && r.json?.ok) {
|
|
163117
|
+
sent++;
|
|
163118
|
+
results.push({ grantId: item.grant.grantId, ok: true });
|
|
163119
|
+
continue;
|
|
163120
|
+
}
|
|
163121
|
+
failed++;
|
|
163122
|
+
keep.push(item);
|
|
163123
|
+
results.push({ grantId: item.grant.grantId, ok: false, error: r.json?.code || r.status });
|
|
163124
|
+
} catch {
|
|
163125
|
+
failed++;
|
|
163126
|
+
keep.push(item);
|
|
163127
|
+
results.push({ grantId: item.grant.grantId, ok: false, error: "network" });
|
|
163128
|
+
}
|
|
163129
|
+
}
|
|
163130
|
+
setQueue(storage2, keep);
|
|
163131
|
+
return { sent, failed, results };
|
|
163132
|
+
}
|
|
163133
|
+
async function authorizeFromPhone(input) {
|
|
163134
|
+
const c = cryptoOf(input.cryptoObj);
|
|
163135
|
+
if (!ed25519Available(c || void 0)) {
|
|
163136
|
+
return { ok: false, error: "device_signing_unavailable: \u672C\u673A WebView \u4E0D\u652F\u6301 Ed25519 \u8BBE\u5907\u7B7E\u540D \u2014\u2014 \u8BF7\u5728\u684C\u9762\u7AEF\u6388\u6743 (\u6216\u5347\u7EA7\u7CFB\u7EDF WebView)" };
|
|
163137
|
+
}
|
|
163138
|
+
const dev = await loadOrCreateDeviceKey(input.storage, c);
|
|
163139
|
+
const preset = presetForChoice(input.choice);
|
|
163140
|
+
const grant = {
|
|
163141
|
+
grantId: `gr-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
|
|
163142
|
+
identityId: input.identityId || `sid-phone-${dev.deviceId.slice(-6)}`,
|
|
163143
|
+
ownerDid: input.ownerDid,
|
|
163144
|
+
...preset,
|
|
163145
|
+
grantedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
163146
|
+
grantedBy: input.grantedBy || "leo",
|
|
163147
|
+
grantedVia: "mobile",
|
|
163148
|
+
deviceIds: [dev.deviceId],
|
|
163149
|
+
status: "active",
|
|
163150
|
+
grantVersion: 1
|
|
163151
|
+
};
|
|
163152
|
+
const signature = await signGrantOnDevice(grant, input.storage, c);
|
|
163153
|
+
const f = input.fetchImpl || fetch;
|
|
163154
|
+
const reg = await registerDeviceOnDesktop(input.base, input.storage, { fetchImpl: f, cryptoObj: c }).catch(() => ({ ok: false }));
|
|
163155
|
+
if (!reg.ok) {
|
|
163156
|
+
const q2 = pendingQueue(input.storage);
|
|
163157
|
+
q2.push({ grant, signature, queuedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
163158
|
+
setQueue(input.storage, q2);
|
|
163159
|
+
return {
|
|
163160
|
+
ok: false,
|
|
163161
|
+
queued: true,
|
|
163162
|
+
error: `\u684C\u9762\u6682\u4E0D\u53EF\u8FBE \u2192 \u6388\u6743\u5DF2\u5B58\u5728\u624B\u673A, \u5F85\u684C\u9762\u5728\u7EBF\u65F6\u81EA\u52A8\u540C\u6B65 (${reg.error || "offline"})`,
|
|
163163
|
+
data: { grantId: grant.grantId, level: grant.level, signature },
|
|
163164
|
+
note: "\u684C\u9762\u4E0D\u5728\u7EBF\u65F6\u4E0D\u5047\u88C5\u5DF2\u751F\u6548: \u6388\u6743\u5728\u672C\u5730\u961F\u5217\u91CC, \u684C\u9762\u4E00\u4E0A\u7EBF\u5C31\u8865\u540C\u6B65"
|
|
163165
|
+
};
|
|
163166
|
+
}
|
|
163167
|
+
const r = await post(input.base, "/api/contacts/grants/sync", { grant: { ...grant, signature } }, f);
|
|
163168
|
+
if (r.status === 200 && r.json?.ok) {
|
|
163169
|
+
return { ok: true, data: { grantId: grant.grantId, level: grant.level, signature }, note: "\u684C\u9762\u5DF2\u9A8C\u7B7E\u63A5\u53D7" };
|
|
163170
|
+
}
|
|
163171
|
+
const q = pendingQueue(input.storage);
|
|
163172
|
+
q.push({ grant, signature, queuedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
163173
|
+
setQueue(input.storage, q);
|
|
163174
|
+
return {
|
|
163175
|
+
ok: false,
|
|
163176
|
+
queued: true,
|
|
163177
|
+
error: `\u684C\u9762\u62D2\u7EDD/\u672A\u63A5\u53D7: ${r.json?.code || r.json?.reason || r.status}`,
|
|
163178
|
+
data: { grantId: grant.grantId, level: grant.level, signature },
|
|
163179
|
+
note: "\u5DF2\u4FDD\u5B58\u5728\u624B\u673A\u672C\u5730\u961F\u5217 (\u4E0D\u4F1A\u56E0\u4E3A\u5931\u8D25\u5C31\u5F53\u6210\u5DF2\u6388\u6743)"
|
|
163180
|
+
};
|
|
163181
|
+
}
|
|
163182
|
+
async function revokeFromPhone(opts) {
|
|
163183
|
+
const c = cryptoOf(opts.cryptoObj);
|
|
163184
|
+
if (!ed25519Available(c || void 0)) return { ok: false, error: "device_signing_unavailable: \u672C\u673A\u4E0D\u652F\u6301\u8BBE\u5907\u7B7E\u540D, \u64A4\u9500\u9700\u5728\u684C\u9762\u7AEF\u8FDB\u884C" };
|
|
163185
|
+
const revokedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
163186
|
+
const payload = { grantId: opts.grantId, grantVersion: opts.version || 1, revokedAt, by: "mobile" };
|
|
163187
|
+
const signature = await signRevocationOnDevice(payload, opts.storage, c);
|
|
163188
|
+
const f = opts.fetchImpl || fetch;
|
|
163189
|
+
const r = await post(opts.base, "/api/contacts/grants/revoke-sync", { grantId: opts.grantId, by: "mobile", reason: opts.reason, version: payload.grantVersion, revokedAt, signature }, f);
|
|
163190
|
+
if (r.status === 200 && r.json?.ok) return { ok: true, data: { grantId: opts.grantId }, note: "\u684C\u9762\u5DF2\u9A8C\u7B7E\u5E76\u7ACB\u5373\u5931\u6548" };
|
|
163191
|
+
return { ok: false, error: `\u64A4\u9500\u672A\u9001\u8FBE: ${r.json?.code || r.json?.error || r.status}`, note: "\u684C\u9762\u4E0D\u5728\u7EBF\u65F6\u64A4\u9500**\u4E0D\u4F1A**\u88AB\u5F53\u4F5C\u5DF2\u5B8C\u6210 \u2014\u2014 \u8BF7\u5728\u684C\u9762\u5728\u7EBF\u540E\u91CD\u8BD5 (\u6216\u684C\u9762\u7AEF\u64A4\u9500)" };
|
|
163192
|
+
}
|
|
163193
|
+
async function loadFromDesktop(base6, storage2, opts = {}) {
|
|
163194
|
+
const f = opts.fetchImpl || fetch;
|
|
163195
|
+
if (!base6) {
|
|
163196
|
+
const caps = loadCapabilityCopy(storage2);
|
|
163197
|
+
return {
|
|
163198
|
+
ok: false,
|
|
163199
|
+
error: "desktop_offline",
|
|
163200
|
+
data: { contacts: caps, grants: [], effective: "\u684C\u9762\u4E0D\u5728\u7EBF (\u663E\u793A\u624B\u673A\u672C\u5730\u526F\u672C)", approvals: [], waiting: [] },
|
|
163201
|
+
note: "\u4EE5\u4E0B\u662F\u4F60\u624B\u673A\u4E0A\u4FDD\u5B58\u7684\u80FD\u529B\u526F\u672C (\u4E0D\u662F\u684C\u9762\u5B9E\u65F6\u72B6\u6001)"
|
|
163202
|
+
};
|
|
163203
|
+
}
|
|
163204
|
+
try {
|
|
163205
|
+
const a = await get(base6, "/api/contacts", f);
|
|
163206
|
+
const b = await get(base6, "/api/contacts/grants", f);
|
|
163207
|
+
if (a.status !== 200 && b.status !== 200) return { ok: false, error: `desktop_http_${a.status || b.status}` };
|
|
163208
|
+
const view = {
|
|
163209
|
+
contacts: a.json?.contacts || [],
|
|
163210
|
+
grants: b.json?.grants || [],
|
|
163211
|
+
effective: b.json?.effective || "",
|
|
163212
|
+
approvals: a.json?.approvals || [],
|
|
163213
|
+
waiting: a.json?.waiting || []
|
|
163214
|
+
};
|
|
163215
|
+
saveCapabilityCopy(storage2, (view.contacts || []).filter((c) => c.capabilities?.includes("send")).map((c) => ({
|
|
163216
|
+
contactId: c.contactId,
|
|
163217
|
+
kind: c.kind,
|
|
163218
|
+
displayValue: c.displayValue,
|
|
163219
|
+
verificationStatus: c.status,
|
|
163220
|
+
capabilities: c.capabilities,
|
|
163221
|
+
provider: c.provider,
|
|
163222
|
+
state: c.status
|
|
163223
|
+
})));
|
|
163224
|
+
return { ok: true, data: view };
|
|
163225
|
+
} catch (err2) {
|
|
163226
|
+
const caps = loadCapabilityCopy(storage2);
|
|
163227
|
+
return {
|
|
163228
|
+
ok: false,
|
|
163229
|
+
error: "desktop_offline",
|
|
163230
|
+
data: { contacts: caps, grants: [], effective: "\u684C\u9762\u4E0D\u5728\u7EBF (\u663E\u793A\u624B\u673A\u672C\u5730\u526F\u672C)", approvals: [], waiting: [] },
|
|
163231
|
+
note: `\u8FDE\u4E0D\u4E0A\u684C\u9762 (${String(err2?.message || err2).slice(0, 60)}) \u2014\u2014 \u663E\u793A\u672C\u5730\u526F\u672C`
|
|
163232
|
+
};
|
|
163233
|
+
}
|
|
163234
|
+
}
|
|
163235
|
+
async function bindFromPhone(base6, opts, storage2, f) {
|
|
163236
|
+
const impl = f || fetch;
|
|
163237
|
+
if (!base6) return { ok: false, error: "desktop_offline: \u7ED1\u5B9A\u9700\u8981\u684C\u9762\u7AEF\u5728\u7EBF (\u684C\u9762\u8D1F\u8D23\u843D\u76D8\u4E0E\u9A8C\u8BC1\u7801\u901A\u9053)" };
|
|
163238
|
+
const r = await post(base6, "/api/contacts/bind", { kind: opts.kind, value: opts.value, region: opts.region }, impl);
|
|
163239
|
+
if (r.status === 200 && r.json?.ok) return { ok: true, data: r.json };
|
|
163240
|
+
return { ok: false, error: r.json?.error || `HTTP ${r.status}` };
|
|
163241
|
+
}
|
|
163242
|
+
async function verifyFromPhone(base6, opts, storage2, f) {
|
|
163243
|
+
const impl = f || fetch;
|
|
163244
|
+
if (!base6) return { ok: false, error: "desktop_offline: \u9A8C\u8BC1\u9700\u8981\u684C\u9762\u7AEF\u5728\u7EBF" };
|
|
163245
|
+
const r = await post(base6, "/api/contacts/verify", { contactId: opts.contactId, challengeId: opts.challengeId, code: opts.code }, impl);
|
|
163246
|
+
if (r.status === 200 && r.json?.ok) return { ok: true, data: r.json };
|
|
163247
|
+
return { ok: false, error: r.json?.error || `HTTP ${r.status}` };
|
|
163248
|
+
}
|
|
163249
|
+
async function decideApprovalFromPhone(base6, opts, f) {
|
|
163250
|
+
const impl = f || fetch;
|
|
163251
|
+
if (!base6) return { ok: false, error: "desktop_offline: \u6279\u51C6\u9700\u8981\u684C\u9762\u7AEF\u5728\u7EBF (\u624B\u673A\u4E0D\u5728\u7EBF\u65F6\u4E0D\u4F1A\u5047\u88C5\u5DF2\u6279\u51C6)" };
|
|
163252
|
+
const r = await post(base6, `/api/contacts/approvals/${encodeURIComponent(opts.consentId)}/${opts.action}`, { by: "leo", via: "mobile", body: opts.body, reason: opts.reason }, impl);
|
|
163253
|
+
if (r.status === 200 && r.json?.ok) return { ok: true, data: r.json };
|
|
163254
|
+
return { ok: false, error: r.json?.error || `HTTP ${r.status}` };
|
|
163255
|
+
}
|
|
163256
|
+
function buildPhoneCard(view, opts) {
|
|
163257
|
+
return {
|
|
163258
|
+
title: "\u8BA9 Bolloon \u4EE3\u8868\u4F60\u8054\u7CFB\u5916\u90E8\u7684\u4EBA",
|
|
163259
|
+
effective: view?.effective || opts.offline || "\u672A\u77E5",
|
|
163260
|
+
contacts: (view?.contacts || []).map((c) => ({
|
|
163261
|
+
displayValue: c.displayValue || "",
|
|
163262
|
+
kind: c.kind || "",
|
|
163263
|
+
state: c.status || c.verificationStatus || "",
|
|
163264
|
+
label: c.capabilities?.includes("send") ? "\u53EF\u8054\u7CFB" : "\u4E0D\u53EF\u8054\u7CFB"
|
|
163265
|
+
})),
|
|
163266
|
+
approvals: (view?.approvals || []).map((a) => ({
|
|
163267
|
+
consentId: a.consentId,
|
|
163268
|
+
contactName: a.contactName,
|
|
163269
|
+
channel: a.channel,
|
|
163270
|
+
subject: a.subject,
|
|
163271
|
+
bodyPreview: a.bodyPreview || "",
|
|
163272
|
+
reason: a.reason,
|
|
163273
|
+
reallySent: !!a.reallySent
|
|
163274
|
+
})),
|
|
163275
|
+
choices: [
|
|
163276
|
+
{ id: "task_once", label: "\u4EC5\u672C\u6B21\u4EFB\u52A1" },
|
|
163277
|
+
{ id: "persistent", label: "\u957F\u671F\u4F7F\u7528 (\u63A8\u8350)" },
|
|
163278
|
+
{ id: "full_contact_access", label: "\u5B8C\u5168\u6388\u6743\u8054\u7CFB\u65B9\u5F0F\u80FD\u529B" }
|
|
163279
|
+
],
|
|
163280
|
+
willNotGet: ["\u8BFB\u53D6\u5168\u90E8\u90AE\u7BB1", "\u8BFB\u53D6\u901A\u8BAF\u5F55", "\u7FA4\u53D1\u6D88\u606F", "\u81EA\u52A8\u652F\u4ED8/\u8F6C\u8D26", "\u4EE3\u8868\u4F60\u7B7E\u7F72\u5408\u540C", "\u62FF\u5230\u624B\u673A\u53F7/\u90AE\u7BB1\u660E\u6587"],
|
|
163281
|
+
deviceSigning: opts.deviceSigning
|
|
163282
|
+
};
|
|
163283
|
+
}
|
|
163284
|
+
|
|
162926
163285
|
// src/web/mobile-core.ts
|
|
162927
163286
|
var busHandlers = /* @__PURE__ */ new Set();
|
|
162928
163287
|
function busBroadcast(msg) {
|
|
@@ -163033,6 +163392,8 @@ ${error.stack}` : head;
|
|
|
163033
163392
|
if (p === "/api/peers") return () => core.peers.list();
|
|
163034
163393
|
if (p === "/api/mcp/tools") return () => core.mcp.tools();
|
|
163035
163394
|
if (p === "/api/skills") return () => core.skills.list();
|
|
163395
|
+
if (p === "/api/contacts") return () => core.contacts.view();
|
|
163396
|
+
if (p === "/api/contacts/grants") return () => core.contacts.grants();
|
|
163036
163397
|
if (p === "/api/auth/status") return () => core.identity.status();
|
|
163037
163398
|
if (p === "/api/payments/pending") return () => core.payments.pending();
|
|
163038
163399
|
if (p === "/api/llm-config") return () => core.data.getLlmConfig();
|
|
@@ -163786,6 +164147,111 @@ ${error.stack}` : head;
|
|
|
163786
164147
|
return mobileGatewayTool2(String(name10 || ""), args || {});
|
|
163787
164148
|
}
|
|
163788
164149
|
},
|
|
164150
|
+
/**
|
|
164151
|
+
* 联系方式与持久授权 (2026-09-19, Phase 5/10)
|
|
164152
|
+
* 手机负责: 输入 · OTP 确认 · 生物识别/系统确认 · 展示待发内容 · 批准高风险联系 · **签名长期授权**
|
|
164153
|
+
* 桌面负责: 落盘 · 执行 · 等待回复 · 证据
|
|
164154
|
+
* 铁律: 明文不进手机存储 (只存 capability 副本), 桌面离线不假装成功, 不支持 Ed25519 就明说。
|
|
164155
|
+
*/
|
|
164156
|
+
contacts: {
|
|
164157
|
+
/** 本机是否能做设备签名 (老 WebView 可能没有 Ed25519) */
|
|
164158
|
+
deviceSigning() {
|
|
164159
|
+
return ed25519Available();
|
|
164160
|
+
},
|
|
164161
|
+
/** 桌面 HTTP 地址 (拿不到 → null, 不猜) */
|
|
164162
|
+
async desktopBase() {
|
|
164163
|
+
try {
|
|
164164
|
+
const u = await core.desktop?.url?.();
|
|
164165
|
+
const raw = u && typeof u === "object" ? u.url || u.baseUrl || "" : u;
|
|
164166
|
+
const s2 = String(raw || "").replace(/\/+$/, "");
|
|
164167
|
+
return s2 || null;
|
|
164168
|
+
} catch {
|
|
164169
|
+
return null;
|
|
164170
|
+
}
|
|
164171
|
+
},
|
|
164172
|
+
/** 手机端本地存储 (只放: 设备私钥 JWK / capability 副本 / 离线队列) */
|
|
164173
|
+
storage() {
|
|
164174
|
+
try {
|
|
164175
|
+
if (typeof localStorage !== "undefined") return localStorage;
|
|
164176
|
+
} catch {
|
|
164177
|
+
}
|
|
164178
|
+
const mem2 = /* @__PURE__ */ new Map();
|
|
164179
|
+
return { getItem: (k) => mem2.has(k) ? mem2.get(k) : null, setItem: (k, v) => {
|
|
164180
|
+
mem2.set(k, v);
|
|
164181
|
+
}, removeItem: (k) => {
|
|
164182
|
+
mem2.delete(k);
|
|
164183
|
+
} };
|
|
164184
|
+
},
|
|
164185
|
+
/** 从桌面读脱敏视图 (桌面离线 → 退回本地 capability 副本并标注) */
|
|
164186
|
+
async view() {
|
|
164187
|
+
const base6 = await core.contacts.desktopBase();
|
|
164188
|
+
const r = await loadFromDesktop(base6, core.contacts.storage());
|
|
164189
|
+
return { ...r.data || { contacts: [], grants: [], effective: "", approvals: [], waiting: [] }, offline: !r.ok, note: r.note, error: r.error };
|
|
164190
|
+
},
|
|
164191
|
+
async grants() {
|
|
164192
|
+
const v = await core.contacts.view();
|
|
164193
|
+
return { grants: v.grants || [], effective: v.effective || "", offline: v.offline };
|
|
164194
|
+
},
|
|
164195
|
+
/** 手机端摘要卡 (给 UI 直接渲染) */
|
|
164196
|
+
async card() {
|
|
164197
|
+
const base6 = await core.contacts.desktopBase();
|
|
164198
|
+
const r = await loadFromDesktop(base6, core.contacts.storage());
|
|
164199
|
+
return buildPhoneCard(r.data || null, { deviceSigning: ed25519Available(), offline: r.error });
|
|
164200
|
+
},
|
|
164201
|
+
/** 确认授权 (三个选项之一) → 设备签名 → 桌面验签; 桌面离线则进本地队列 */
|
|
164202
|
+
async authorize(choice, ownerDid) {
|
|
164203
|
+
const base6 = await core.contacts.desktopBase();
|
|
164204
|
+
let did = ownerDid;
|
|
164205
|
+
if (!did) {
|
|
164206
|
+
try {
|
|
164207
|
+
const st = await core.identity?.status?.();
|
|
164208
|
+
did = st?.did;
|
|
164209
|
+
} catch {
|
|
164210
|
+
}
|
|
164211
|
+
}
|
|
164212
|
+
return authorizeFromPhone({
|
|
164213
|
+
base: base6 || "",
|
|
164214
|
+
choice,
|
|
164215
|
+
ownerDid: did || "did:bolln:local",
|
|
164216
|
+
storage: core.contacts.storage(),
|
|
164217
|
+
grantedBy: "leo"
|
|
164218
|
+
});
|
|
164219
|
+
},
|
|
164220
|
+
/** 手机撤销 (带签名; 桌面离线不假装已撤销) */
|
|
164221
|
+
async revoke(grantId, reason) {
|
|
164222
|
+
const base6 = await core.contacts.desktopBase();
|
|
164223
|
+
if (!base6) return { ok: false, error: "desktop_offline: \u64A4\u9500\u9700\u8981\u684C\u9762\u5728\u7EBF" };
|
|
164224
|
+
return revokeFromPhone({ base: base6, grantId, reason, storage: core.contacts.storage(), version: Date.now() });
|
|
164225
|
+
},
|
|
164226
|
+
async bind(kind, value2, region) {
|
|
164227
|
+
const base6 = await core.contacts.desktopBase();
|
|
164228
|
+
return bindFromPhone(base6, { kind, value: value2, region }, core.contacts.storage());
|
|
164229
|
+
},
|
|
164230
|
+
async verify(contactId, challengeId, code10) {
|
|
164231
|
+
const base6 = await core.contacts.desktopBase();
|
|
164232
|
+
return verifyFromPhone(base6, { contactId, challengeId, code: code10 }, core.contacts.storage());
|
|
164233
|
+
},
|
|
164234
|
+
/** 手机上批准/拒绝高风险联系 (展示待发内容后) */
|
|
164235
|
+
async decide(consentId, action, opts = {}) {
|
|
164236
|
+
const base6 = await core.contacts.desktopBase();
|
|
164237
|
+
return decideApprovalFromPhone(base6, { consentId, action, ...opts });
|
|
164238
|
+
},
|
|
164239
|
+
/** 桌面回来后补同步排队中的授权 */
|
|
164240
|
+
async flushQueue() {
|
|
164241
|
+
const base6 = await core.contacts.desktopBase();
|
|
164242
|
+
if (!base6) return { sent: 0, failed: pendingQueue(core.contacts.storage()).length, results: [] };
|
|
164243
|
+
return flushQueuedGrants(base6, core.contacts.storage());
|
|
164244
|
+
},
|
|
164245
|
+
queue() {
|
|
164246
|
+
return pendingQueue(core.contacts.storage());
|
|
164247
|
+
},
|
|
164248
|
+
capabilities() {
|
|
164249
|
+
return loadCapabilityCopy(core.contacts.storage());
|
|
164250
|
+
},
|
|
164251
|
+
device() {
|
|
164252
|
+
return loadOrCreateDeviceKey(core.contacts.storage());
|
|
164253
|
+
}
|
|
164254
|
+
},
|
|
163789
164255
|
skills: {
|
|
163790
164256
|
async list() {
|
|
163791
164257
|
const s2 = await Promise.resolve().then(() => (init_mobile_sync(), mobile_sync_exports));
|
package/dist/web/mobile.html
CHANGED
|
@@ -121,6 +121,7 @@
|
|
|
121
121
|
<div class="list-item" id="item-settings"><span class="list-icon"><svg class="ico" viewBox="0 0 24 24"><path d="M4 7.5h6M14 7.5h6M4 16.5h9M17 16.5h3"/><circle cx="12" cy="7.5" r="2.2"/><circle cx="15" cy="16.5" r="2.2"/></svg></span><span>设置</span><span class="list-arrow">›</span></div>
|
|
122
122
|
<div class="list-item" id="item-wallet"><span class="list-icon"><svg class="ico" viewBox="0 0 24 24"><rect x="3" y="6.5" width="18" height="12" rx="3"/><path d="M3 10.5h18"/><circle cx="16.8" cy="14.5" r="1.3" fill="currentColor" stroke="none"/></svg></span><span>钱包</span><span class="list-arrow">›</span></div>
|
|
123
123
|
<div class="list-item" id="item-judgments"><span class="list-icon"><svg class="ico" viewBox="0 0 24 24"><path d="M11 3.5l1.8 4.7 4.7 1.8-4.7 1.8L11 16.5l-1.8-4.7L4.5 10l4.7-1.8z"/><path d="M18.5 16l.6 1.6 1.6.6-1.6.6-.6 1.6-.6-1.6-1.6-.6 1.6-.6z"/></svg></span><span>判断力 API</span><span class="list-arrow">›</span></div>
|
|
124
|
+
<div class="list-item" id="item-contacts"><span class="list-icon"><svg class="ico" viewBox="0 0 24 24"><rect x="3.5" y="5.5" width="17" height="13" rx="2.5"/><path d="M3.5 9h17"/><circle cx="8.5" cy="14" r="1.6"/><path d="M12 13h5M12 15.5h3"/></svg></span><span>联系方式与授权</span><span class="list-arrow" id="contacts-arrow">›</span></div>
|
|
124
125
|
<div class="list-item" id="item-login"><span class="list-icon"><svg class="ico" viewBox="0 0 24 24"><rect x="4.5" y="10.5" width="15" height="9.5" rx="2.5"/><path d="M8 10.5V7.5a4 4 0 0 1 8 0v3"/></svg></span><span id="login-label">登录</span><span class="list-arrow">›</span></div>
|
|
125
126
|
<div class="list-item" id="item-logout"><span class="list-icon"><svg class="ico" viewBox="0 0 24 24"><path d="M14.5 4.5h3.5a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2h-3.5"/><path d="M10 8.5l-3.5 3.5L10 15.5M6.5 12h8"/></svg></span><span>注销</span></div>
|
|
126
127
|
</div>
|
|
@@ -235,3 +236,34 @@ if ('serviceWorker' in navigator) {
|
|
|
235
236
|
</script>
|
|
236
237
|
</body>
|
|
237
238
|
</html>
|
|
239
|
+
|
|
240
|
+
<!-- 联系方式与持久授权 (2026-09-19): 手机确认一次, Agent 长期自动使用 -->
|
|
241
|
+
<div class="sheet" id="contacts-sheet" hidden>
|
|
242
|
+
<div class="sheet-inner">
|
|
243
|
+
<div class="sheet-title">联系方式与授权</div>
|
|
244
|
+
<div class="sheet-text" id="contacts-effective">加载中...</div>
|
|
245
|
+
<div class="sheet-text" id="contacts-signing" style="font-size:12px;color:var(--text-secondary)"></div>
|
|
246
|
+
|
|
247
|
+
<div id="contacts-approvals"></div>
|
|
248
|
+
|
|
249
|
+
<div class="sheet-title" style="margin-top:10px">我的联系方式</div>
|
|
250
|
+
<div id="contacts-list"><div class="sheet-text">加载中...</div></div>
|
|
251
|
+
|
|
252
|
+
<div style="margin-top:10px">
|
|
253
|
+
<input id="contacts-value" placeholder="手机号 (+86...) 或 邮箱" style="width:100%;padding:10px 12px;border-radius:10px;border:1px solid var(--border);background:transparent;color:var(--text);font-size:14px" />
|
|
254
|
+
<div style="display:flex;gap:8px;margin-top:8px">
|
|
255
|
+
<button class="sheet-choice" id="contacts-bind" style="flex:1">绑定并发送验证码</button>
|
|
256
|
+
</div>
|
|
257
|
+
<input id="contacts-code" placeholder="收到的验证码" inputmode="numeric" style="width:100%;margin-top:8px;padding:10px 12px;border-radius:10px;border:1px solid var(--border);background:transparent;color:var(--text);font-size:14px" />
|
|
258
|
+
<button class="sheet-choice" id="contacts-verify" style="margin-top:8px;width:100%">验证</button>
|
|
259
|
+
</div>
|
|
260
|
+
|
|
261
|
+
<div class="sheet-title" style="margin-top:12px">让 Bolloon 代表你联系外部的人</div>
|
|
262
|
+
<div class="sheet-text" id="contacts-willnot" style="font-size:12px"></div>
|
|
263
|
+
<div class="sheet-inner-choices" id="contacts-choices" style="margin-top:8px"></div>
|
|
264
|
+
<div class="sheet-inner-choices">
|
|
265
|
+
<button class="sheet-choice" id="contacts-revoke-all">撤销全部授权</button>
|
|
266
|
+
<button class="sheet-choice sheet-cancel" id="contacts-close">关闭</button>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|