@hellolynk-ai/codex-plugin 0.0.0-stage → 0.2.100
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/.agents/plugins/marketplace.json +20 -0
- package/LICENSE +92 -0
- package/README.md +101 -2
- package/bin/hellolynk-client-setup.mjs +2748 -0
- package/bin/hellolynk-mcp-config-shape.mjs +23 -0
- package/hellolynk-client-pack-manifest.json +619 -0
- package/hellolynk-client-pack-pin.json +4 -0
- package/package.json +58 -4
- package/plugins/hellolynk-codex/.app.json +3 -0
- package/plugins/hellolynk-codex/.codex-plugin/plugin.json +38 -0
- package/plugins/hellolynk-codex/.mcp.json +19 -0
- package/plugins/hellolynk-codex/assets/hellolynk-mark.png +0 -0
- package/plugins/hellolynk-codex/package.json +11 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-account.mjs +180 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-core.mjs +770 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-profile.mjs +191 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-host-auth.mjs +347 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-host-protected-store.mjs +2721 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-local-process-lock.mjs +157 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-neutral-host-installation-store.mjs +765 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-platform-credential-store.mjs +773 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-runtime-safety.cjs +471 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
- package/plugins/hellolynk-codex/runtime/connect-activate.mjs +837 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-agent-configurator.mjs +701 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-account.mjs +180 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-core.mjs +770 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-profile.mjs +191 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-entry.mjs +31 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-runtime.mjs +600 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update.mjs +453 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-contact-request-e2ee.mjs +867 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-device-enrollment.mjs +435 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-direct-text-e2ee-client.mjs +1964 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-direct-text-e2ee.mjs +1207 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host-auth.mjs +347 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host-protected-store.mjs +2721 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host-transport-client.mjs +946 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host.mjs +2374 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-human-confirmation-ledger.mjs +256 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-human-relationship-controller.mjs +1234 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-inbox-read-state.mjs +11 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-indexed-message-store.mjs +1558 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-instance-conversation.mjs +185 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-contact-requests.mjs +1733 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-contacts.mjs +1228 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-data-migration.mjs +766 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-data-protection.mjs +747 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-identity-trust.mjs +274 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-messaging.mjs +3043 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-peer-transport.mjs +103 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-process-lock.mjs +157 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-model-inbox.mjs +230 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-model-notices.cjs +60 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-addressing-tools.cjs +231 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-conversations.mjs +280 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-human-confirmation-broker.mjs +1036 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-inbox-runtime.mjs +553 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-notice.mjs +50 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-reply-routes.mjs +96 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-worker-lease.mjs +84 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-assembly.mjs +955 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-installation-store.mjs +765 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-launcher.mjs +665 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-process.mjs +4374 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-store-adapters.mjs +548 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-workbench-read-model.mjs +1365 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-platform-credential-store.mjs +773 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-relay-client-cli.mjs +232 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-release-retention.mjs +277 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-runtime-safety.cjs +471 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-workbench-action-grants.mjs +457 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-workbench-connection.mjs +234 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-bootstrap.mjs +64 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-release-entry.mjs +70 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-release-loader.mjs +156 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-release-manifest.json +358 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-release-pin.json +4 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/configure-agent.ps1 +281 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/credential-store.ps1 +844 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-human-confirmation-ledger.ps1 +155 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-local-data-key.ps1 +300 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-native-human-confirmation-broker.ps1 +216 -0
- package/plugins/hellolynk-codex/runtime/workbench/candidate-bootstrap.mjs +54 -0
- package/plugins/hellolynk-codex/runtime/workbench/contract.mjs +1314 -0
- package/plugins/hellolynk-codex/runtime/workbench/hellolynk-workbench-build.json +215 -0
- package/plugins/hellolynk-codex/runtime/workbench/host-capabilities.mjs +80 -0
- package/plugins/hellolynk-codex/runtime/workbench/i18n.mjs +1127 -0
- package/plugins/hellolynk-codex/runtime/workbench/loopback-surface.mjs +740 -0
- package/plugins/hellolynk-codex/runtime/workbench/mcp-app-bundle.mjs +97 -0
- package/plugins/hellolynk-codex/runtime/workbench/service.mjs +485 -0
- package/plugins/hellolynk-codex/runtime/workbench/state.mjs +421 -0
- package/plugins/hellolynk-codex/runtime/workbench/transports.mjs +171 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/app.mjs +6120 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/connection-form.mjs +155 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/hellolynk-mark.png +0 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/http-transport.mjs +247 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/arrow-left.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/close.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/contacts.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/inbox.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/refresh.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/search.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/security.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/settings.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/index.html +139 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/loopback-entry.mjs +26 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/mcp-app-client.mjs +151 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/mcp-app-entry.mjs +452 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/styles.css +686 -0
- package/plugins/hellolynk-codex/server/codex-neutral-production.mjs +520 -0
- package/plugins/hellolynk-codex/server/mcp-proxy.mjs +306 -0
- package/plugins/hellolynk-codex/server/mcp-server.mjs +1193 -0
- package/plugins/hellolynk-codex/server/native-conversation-directory.mjs +33 -0
- package/plugins/hellolynk-codex/server/native-reply-routes.mjs +32 -0
- package/plugins/hellolynk-codex/server/native-task-client.mjs +382 -0
- package/plugins/hellolynk-codex/server/native-task-runtime.mjs +8 -0
- package/plugins/hellolynk-codex/server/neutral-host-adapter.mjs +649 -0
- package/plugins/hellolynk-codex/server/workbench-runtime.mjs +180 -0
- package/plugins/hellolynk-codex/skills/hellolynk-open/SKILL.md +11 -0
- package/plugins/hellolynk-codex/skills/hellolynk-open/agents/openai.yaml +4 -0
- package/tools/hellolynk-activate-neutral-host-clients.mjs +837 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
const ID_PATTERN = /^hellolynk:(intl|cn):[a-z0-9][a-z0-9._-]{2,179}$/;
|
|
2
|
+
const CONFIRMATION_URL = /^https:\/\/hellolynk\.com\/account\?deviceRequest=devreq_[a-f0-9]{48}$/;
|
|
3
|
+
function acceptedInstanceName(value) {
|
|
4
|
+
// Keep exact saved-intent names available for continuation. Only the enrollment
|
|
5
|
+
// coordinator knows whether to apply the stricter new-name rule.
|
|
6
|
+
if ([...value].length > 80
|
|
7
|
+
|| /[\u0000-\u001f\u007f-\u009f\u061c\u200e\u200f\u2028-\u202e\u2066-\u2069]/u.test(value)) return null;
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function createWorkbenchConnectionForm(options) {
|
|
12
|
+
const t = options.translate ?? ((key) => key);
|
|
13
|
+
let connectionForm = null;
|
|
14
|
+
const startConnection = function startConnection() {
|
|
15
|
+
if (connectionForm) { connectionForm.querySelector("input")?.focus(); return; }
|
|
16
|
+
const returnFocus = document.getElementById("connection-start") || document.activeElement;
|
|
17
|
+
const form = document.createElement("div");
|
|
18
|
+
form.className = "connection-fields";
|
|
19
|
+
form.setAttribute("role", "form");
|
|
20
|
+
form.innerHTML = `<a data-account href="https://hellolynk.com/account" target="_blank" rel="noopener noreferrer"></a>
|
|
21
|
+
<label class="form-field"><span data-id-label class="form-label"></span><input data-id class="form-input" type="text" autocomplete="off" spellcheck="false" placeholder="hellolynk:intl:your-id" maxlength="195"></label>
|
|
22
|
+
<label class="form-field"><span data-name-label class="form-label"></span><input data-name class="form-input" type="text" autocomplete="off" maxlength="80"><small data-name-help></small></label>
|
|
23
|
+
<strong data-confirm-title class="connection-confirmation-title" hidden></strong>
|
|
24
|
+
<p data-status class="connection-help" role="status" aria-live="polite"></p>
|
|
25
|
+
<a data-confirm class="primary-button connection-confirm-button" target="_blank" rel="noopener noreferrer" hidden></a>
|
|
26
|
+
<p data-confirm-help class="connection-help" hidden></p>
|
|
27
|
+
<button data-submit type="button" class="primary-button"></button>
|
|
28
|
+
<button data-cancel type="button" class="secondary-button"></button>`;
|
|
29
|
+
const field = (name) => form.querySelector(`[data-${name}]`);
|
|
30
|
+
const id = field("id"), name = field("name"), status = field("status"), submit = field("submit"), confirmation = field("confirm");
|
|
31
|
+
let running = false;
|
|
32
|
+
let waiting = false;
|
|
33
|
+
let confirmInput = null;
|
|
34
|
+
let statusKey = null;
|
|
35
|
+
let statusCode = null;
|
|
36
|
+
let idValidationKey = null;
|
|
37
|
+
let nameValidationKey = null;
|
|
38
|
+
const message = (key) => key ? t(`connectionForm.message.${key}`) : "";
|
|
39
|
+
function render() {
|
|
40
|
+
field("id-label").textContent = t("connectionForm.idLabel");
|
|
41
|
+
field("account").textContent = t("connectionForm.account");
|
|
42
|
+
field("name-label").textContent = t("connectionForm.nameLabel");
|
|
43
|
+
field("name-help").textContent = t("connectionForm.nameHelp");
|
|
44
|
+
name.placeholder = t("connectionForm.namePlaceholder");
|
|
45
|
+
field("confirm-title").hidden = !waiting;
|
|
46
|
+
field("confirm-title").textContent = t("connectionForm.confirmTitle");
|
|
47
|
+
field("confirm-help").hidden = !waiting;
|
|
48
|
+
field("confirm-help").textContent = t("connectionForm.confirmHelp");
|
|
49
|
+
confirmation.textContent = t("connectionForm.step1");
|
|
50
|
+
submit.className = waiting ? "secondary-button" : "primary-button";
|
|
51
|
+
submit.textContent = running ? t("connectionForm.connectingButton") : waiting ? t("connectionForm.step2") : t("connectionForm.connect");
|
|
52
|
+
field("cancel").textContent = t("action.cancel");
|
|
53
|
+
status.textContent = message(statusKey)
|
|
54
|
+
+ (statusCode ? `\n${t("connectionForm.errorCode")}${statusCode}` : "");
|
|
55
|
+
// A failed attempt must look like one; "connecting" and "confirm first" stay neutral.
|
|
56
|
+
status.className = statusCode ? "connection-help is-error" : "connection-help";
|
|
57
|
+
id.setCustomValidity(message(idValidationKey));
|
|
58
|
+
name.setCustomValidity(message(nameValidationKey));
|
|
59
|
+
}
|
|
60
|
+
const observer = new MutationObserver(render);
|
|
61
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] });
|
|
62
|
+
function close() {
|
|
63
|
+
observer.disconnect();
|
|
64
|
+
form.remove();
|
|
65
|
+
if (connectionForm === form) connectionForm = null;
|
|
66
|
+
}
|
|
67
|
+
startConnection.close = close;
|
|
68
|
+
function invalidateConfirmation() {
|
|
69
|
+
idValidationKey = null; nameValidationKey = null;
|
|
70
|
+
id.setCustomValidity("");
|
|
71
|
+
name.setCustomValidity("");
|
|
72
|
+
if (waiting) {
|
|
73
|
+
waiting = false; confirmInput = null; confirmation.hidden = true; confirmation.removeAttribute("href"); statusKey = null; statusCode = null; render();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function connect() {
|
|
77
|
+
if (running) return;
|
|
78
|
+
const instanceName = acceptedInstanceName(name.value);
|
|
79
|
+
const input = { hellolynkId: id.value.trim(), instanceName: instanceName ?? name.value };
|
|
80
|
+
idValidationKey = ID_PATTERN.test(input.hellolynkId) ? null : "idRequired";
|
|
81
|
+
nameValidationKey = instanceName !== null ? null : "nameInvalid";
|
|
82
|
+
render();
|
|
83
|
+
if (!id.reportValidity()) return;
|
|
84
|
+
if (!name.reportValidity()) return;
|
|
85
|
+
if (confirmInput && JSON.stringify(confirmInput) !== JSON.stringify(input)) invalidateConfirmation();
|
|
86
|
+
running = true; id.disabled = true; name.disabled = true; submit.disabled = true; field("cancel").disabled = true;
|
|
87
|
+
statusKey = "connecting"; statusCode = null;
|
|
88
|
+
render();
|
|
89
|
+
try {
|
|
90
|
+
const result = await options.connect(input);
|
|
91
|
+
if (result.status === "confirmation_required" && CONFIRMATION_URL.test(result.verificationUrl)) {
|
|
92
|
+
waiting = true; confirmInput = input;
|
|
93
|
+
confirmation.href = result.verificationUrl; confirmation.hidden = false;
|
|
94
|
+
statusKey = "confirmationRequired"; statusCode = null;
|
|
95
|
+
} else if (result.status === "registered") {
|
|
96
|
+
close();
|
|
97
|
+
await options.onConnected(result);
|
|
98
|
+
} else throw new Error("workbench_connect_result_invalid");
|
|
99
|
+
} catch (error) {
|
|
100
|
+
const value = String(error?.code || error?.message || "workbench_connect_failed");
|
|
101
|
+
const safe = /^[a-z][a-z0-9_]{1,127}$/.test(value) ? value : "workbench_connect_failed";
|
|
102
|
+
statusKey = safe === "network_certificate_untrusted" ? "networkCertificate"
|
|
103
|
+
: ["network_name_resolution_failed", "network_connection_refused"].includes(safe) ? "networkAddress"
|
|
104
|
+
: ["network_proxy_runtime_unsupported", "network_proxy_configuration_invalid", "network_system_proxy_unavailable"].includes(safe) ? "networkProxy"
|
|
105
|
+
: ["device_enrollment_name_invalid", "agent_instance_name_invalid"].includes(safe) ? "enrollmentNameInvalid"
|
|
106
|
+
: safe === "agent_instance_name_conflict" ? "nameTaken"
|
|
107
|
+
: safe === "workbench_connect_name_invalid" ? "nameInvalid"
|
|
108
|
+
: ["workbench_connect_id_invalid", "device_enrollment_hellolynk_id_invalid"].includes(safe) ? "idInvalid"
|
|
109
|
+
: ["workbench_connect_busy", "device_enrollment_busy"].includes(safe) ? "busy"
|
|
110
|
+
// powershell7_missing is certain. credential_helper_unavailable is thrown for any helper start failure
|
|
111
|
+
// (most likely no PowerShell 7), so it gets a "may not be installed" message. Retrying cannot help either,
|
|
112
|
+
// so neither may fall into the generic "unavailable" bucket below.
|
|
113
|
+
: safe === "powershell7_missing" ? "powershell7Missing"
|
|
114
|
+
: safe === "credential_helper_unavailable" ? "credentialHelperUnavailable"
|
|
115
|
+
: safe === "device_enrollment_existing_profile_requires_setup" ? "existingSetup"
|
|
116
|
+
: safe === "device_enrollment_intent_conflict" ? "savedConnectionConflict"
|
|
117
|
+
: safe === "device_enrollment_authorization_expired" ? "authorizationExpired"
|
|
118
|
+
: /timeout|request_failed|unavailable/.test(safe) ? "resultPending"
|
|
119
|
+
: /conflict|existing_profile|owner_mismatch/.test(safe) ? "existingConnection"
|
|
120
|
+
: "failed";
|
|
121
|
+
statusCode = safe;
|
|
122
|
+
if (["device_enrollment_name_invalid", "workbench_connect_name_invalid", "agent_instance_name_invalid", "agent_instance_name_conflict"].includes(safe)) nameValidationKey = statusKey;
|
|
123
|
+
} finally {
|
|
124
|
+
running = false; id.disabled = false; name.disabled = false; submit.disabled = false; field("cancel").disabled = false; render();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
submit.addEventListener("click", () => { void connect(); });
|
|
128
|
+
field("cancel").addEventListener("click", () => {
|
|
129
|
+
close();
|
|
130
|
+
if (returnFocus?.isConnected && !returnFocus.hidden && !returnFocus.disabled
|
|
131
|
+
&& !returnFocus.closest('[hidden], [inert], [aria-hidden="true"]')) {
|
|
132
|
+
returnFocus.focus({ preventScroll: true });
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
for (const input of [id, name]) {
|
|
136
|
+
input.addEventListener("input", invalidateConfirmation);
|
|
137
|
+
input.addEventListener("keydown", (event) => {
|
|
138
|
+
if (event.key === "Enter" && !event.isComposing) { event.preventDefault(); void connect(); }
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
for (const link of [field("account"), confirmation]) {
|
|
142
|
+
if (typeof options.openLink === "function") link.addEventListener("click", (event) => {
|
|
143
|
+
event.preventDefault();
|
|
144
|
+
void Promise.resolve().then(() => options.openLink(link.href)).catch(() => {
|
|
145
|
+
statusKey = "websiteOpenFailed"; statusCode = null; render();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
render();
|
|
150
|
+
document.getElementById("connection-guide").append(form);
|
|
151
|
+
connectionForm = form;
|
|
152
|
+
id.focus();
|
|
153
|
+
};
|
|
154
|
+
return startConnection;
|
|
155
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
export function createWorkbenchHttpTransport(options = {}) {
|
|
2
|
+
const requestTimeoutMs = positiveTimeout(options.requestTimeoutMs, 10_000);
|
|
3
|
+
// Contact-request admission performs three bounded Host stages: an
|
|
4
|
+
// authoritative metadata preflight, reserve, and (when admitted) relay.
|
|
5
|
+
// Keep explicit test/embedding overrides inherited from requestTimeoutMs.
|
|
6
|
+
const contactRequestTimeoutMs = positiveTimeout(
|
|
7
|
+
options.contactRequestTimeoutMs ?? options.requestTimeoutMs,
|
|
8
|
+
30_000,
|
|
9
|
+
);
|
|
10
|
+
// Page remounts can queue behind an earlier Host read, so snapshots have a
|
|
11
|
+
// separate longer deadline. Ordinary commands keep the base request budget;
|
|
12
|
+
// contact-request admission uses the bounded exception above.
|
|
13
|
+
const snapshotTimeoutMs = positiveTimeout(options.snapshotTimeoutMs ?? options.requestTimeoutMs, 30_000);
|
|
14
|
+
// A first Windows Host launch may take up to 75 seconds before discovery.
|
|
15
|
+
// Once data has loaded, keep the shorter ordinary read deadline.
|
|
16
|
+
const initialSnapshotTimeoutMs = positiveTimeout(options.initialSnapshotTimeoutMs
|
|
17
|
+
?? options.snapshotTimeoutMs ?? options.requestTimeoutMs, 90_000);
|
|
18
|
+
const initialSourceMode = options?.initialSourceMode == null
|
|
19
|
+
? null
|
|
20
|
+
: String(options.initialSourceMode);
|
|
21
|
+
if (initialSourceMode !== null && initialSourceMode !== "synthetic-preview") {
|
|
22
|
+
throw new Error("workbench_http_transport_initial_source_mode_invalid");
|
|
23
|
+
}
|
|
24
|
+
let csrf = null;
|
|
25
|
+
let accountRevision = null;
|
|
26
|
+
let snapshotLoaded = false;
|
|
27
|
+
|
|
28
|
+
async function request(path, options = {}, { knownFailureOnHttpError = false, timeoutMs = requestTimeoutMs } = {}) {
|
|
29
|
+
const controller = new AbortController();
|
|
30
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
31
|
+
try {
|
|
32
|
+
const response = await fetch(path, {
|
|
33
|
+
credentials: "same-origin",
|
|
34
|
+
...options,
|
|
35
|
+
headers: { ...(options.headers || {}), ...(accountRevision ? { "x-hellolynk-account-revision": accountRevision } : {}) },
|
|
36
|
+
signal: controller.signal,
|
|
37
|
+
});
|
|
38
|
+
let body;
|
|
39
|
+
try {
|
|
40
|
+
body = await response.json();
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (controller.signal.aborted) throw error;
|
|
43
|
+
if (response.ok) throw codeError("workbench_loopback_response_invalid");
|
|
44
|
+
body = {};
|
|
45
|
+
}
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
const error = codeError(publicCode(body?.error, "workbench_loopback_failed"));
|
|
48
|
+
if (knownFailureOnHttpError || knownPreMutationError(error.code)) error.outcome = "known-failed";
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) throw codeError("workbench_loopback_response_invalid");
|
|
52
|
+
return body;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (controller.signal.aborted) {
|
|
55
|
+
const timeoutError = new Error("workbench_loopback_timeout");
|
|
56
|
+
timeoutError.code = "workbench_loopback_timeout";
|
|
57
|
+
if (knownFailureOnHttpError) timeoutError.outcome = "known-failed";
|
|
58
|
+
throw timeoutError;
|
|
59
|
+
}
|
|
60
|
+
if (error instanceof TypeError && !error.code) throw codeError("workbench_loopback_unreachable");
|
|
61
|
+
throw error;
|
|
62
|
+
} finally {
|
|
63
|
+
clearTimeout(timeout);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return Object.freeze({
|
|
67
|
+
mode: "loopback-http",
|
|
68
|
+
...(initialSourceMode ? { initialSourceMode } : {}),
|
|
69
|
+
actionGrantMode: "host-single-use-v1",
|
|
70
|
+
async readAccountState() {
|
|
71
|
+
const value = await request("/api/v1/account");
|
|
72
|
+
if (accountRevision !== value.revision) snapshotLoaded = false;
|
|
73
|
+
accountRevision = value.revision;
|
|
74
|
+
return value;
|
|
75
|
+
},
|
|
76
|
+
async disconnectAccount(input) {
|
|
77
|
+
csrf = (await request("/api/v1/csrf")).csrf;
|
|
78
|
+
const value = await request("/api/v1/disconnect", { method: "POST",
|
|
79
|
+
headers: { "content-type": "application/json", "x-hellolynk-csrf": csrf }, body: JSON.stringify(input),
|
|
80
|
+
}, { timeoutMs: 60_000 });
|
|
81
|
+
accountRevision = value.revision;
|
|
82
|
+
snapshotLoaded = false;
|
|
83
|
+
return value;
|
|
84
|
+
},
|
|
85
|
+
async readSnapshot(input = {}) {
|
|
86
|
+
const query = new URLSearchParams();
|
|
87
|
+
for (const key of ["threadId", "threadCursor", "messageCursor", "contactRequestCursor", "query", "filter", "historyBefore"]) {
|
|
88
|
+
if (!Object.hasOwn(input, key) || input[key] === "") continue;
|
|
89
|
+
if (input[key] === null) {
|
|
90
|
+
if (key === "contactRequestCursor" || key === "historyBefore") query.set(key, "");
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
query.set(key, input[key]);
|
|
94
|
+
}
|
|
95
|
+
const suffix = query.size ? `?${query}` : "";
|
|
96
|
+
const revision = accountRevision;
|
|
97
|
+
const body = await request(`/api/v1/snapshot${suffix}`, {}, {
|
|
98
|
+
timeoutMs: snapshotLoaded ? snapshotTimeoutMs : initialSnapshotTimeoutMs,
|
|
99
|
+
});
|
|
100
|
+
if (!body.snapshot || typeof body.snapshot !== "object" || Array.isArray(body.snapshot)) {
|
|
101
|
+
throw codeError("workbench_loopback_response_invalid");
|
|
102
|
+
}
|
|
103
|
+
if (revision === accountRevision) snapshotLoaded = true;
|
|
104
|
+
return body.snapshot;
|
|
105
|
+
},
|
|
106
|
+
async connectAccount(input) {
|
|
107
|
+
csrf = (await request("/api/v1/csrf")).csrf;
|
|
108
|
+
return request("/api/v1/connect", { method: "POST",
|
|
109
|
+
headers: { "content-type": "application/json", "x-hellolynk-csrf": csrf },
|
|
110
|
+
body: JSON.stringify(input),
|
|
111
|
+
}, { timeoutMs: 160_000 });
|
|
112
|
+
},
|
|
113
|
+
async openAccount(url) {
|
|
114
|
+
csrf = (await request("/api/v1/csrf")).csrf;
|
|
115
|
+
return request("/api/v1/open-account", { method: "POST",
|
|
116
|
+
headers: { "content-type": "application/json", "x-hellolynk-csrf": csrf },
|
|
117
|
+
body: JSON.stringify({ url }),
|
|
118
|
+
}, { timeoutMs: 15_000 });
|
|
119
|
+
},
|
|
120
|
+
async executeCommand(command) {
|
|
121
|
+
let prepared = false;
|
|
122
|
+
try {
|
|
123
|
+
if (!csrf) csrf = (await request("/api/v1/csrf", {}, { knownFailureOnHttpError: true })).csrf;
|
|
124
|
+
const headers = {
|
|
125
|
+
"content-type": "application/json",
|
|
126
|
+
"x-hellolynk-csrf": csrf,
|
|
127
|
+
};
|
|
128
|
+
let actionGrant;
|
|
129
|
+
if (command.command !== "message.send-status") {
|
|
130
|
+
const grantResponse = await request("/api/v1/action-grants", {
|
|
131
|
+
method: "POST",
|
|
132
|
+
headers,
|
|
133
|
+
body: JSON.stringify({ command }),
|
|
134
|
+
}, { knownFailureOnHttpError: true });
|
|
135
|
+
actionGrant = privateActionGrant(grantResponse.actionGrant);
|
|
136
|
+
}
|
|
137
|
+
prepared = true;
|
|
138
|
+
return (await request("/api/v1/command", {
|
|
139
|
+
method: "POST",
|
|
140
|
+
headers,
|
|
141
|
+
body: JSON.stringify({ command, ...(actionGrant ? { actionGrant } : {}) }),
|
|
142
|
+
}, {
|
|
143
|
+
timeoutMs: command.command === "contact-request.send"
|
|
144
|
+
? contactRequestTimeoutMs
|
|
145
|
+
: requestTimeoutMs,
|
|
146
|
+
})).result;
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if (!prepared && error && typeof error === "object" && error.outcome == null) error.outcome = "known-failed";
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
subscribe(onChange, onError, onReconnect) {
|
|
153
|
+
const events = new EventSource("/api/v1/events");
|
|
154
|
+
let stopped = false;
|
|
155
|
+
let reconnecting = false;
|
|
156
|
+
let sourceFailed = false;
|
|
157
|
+
let sourceReady = false;
|
|
158
|
+
let keepAliveTimer;
|
|
159
|
+
const recovered = () => {
|
|
160
|
+
if (stopped || sourceFailed || !reconnecting) return;
|
|
161
|
+
reconnecting = false;
|
|
162
|
+
onReconnect?.();
|
|
163
|
+
};
|
|
164
|
+
const failed = (error) => {
|
|
165
|
+
if (stopped) return;
|
|
166
|
+
reconnecting = true;
|
|
167
|
+
onError?.(error);
|
|
168
|
+
};
|
|
169
|
+
const keepSessionAlive = async () => {
|
|
170
|
+
try {
|
|
171
|
+
await request("/api/v1/session", {}, { knownFailureOnHttpError: true });
|
|
172
|
+
if (events.readyState === EventSource.OPEN) recovered();
|
|
173
|
+
} catch (error) {
|
|
174
|
+
failed(error);
|
|
175
|
+
if (error?.code === "workbench_surface_session_invalid") {
|
|
176
|
+
stopped = true;
|
|
177
|
+
events.close();
|
|
178
|
+
}
|
|
179
|
+
} finally {
|
|
180
|
+
if (!stopped) keepAliveTimer = setTimeout(keepSessionAlive, 60_000);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
events.addEventListener("changed", (event) => {
|
|
184
|
+
if (stopped) return;
|
|
185
|
+
try { onChange(JSON.parse(event.data)); }
|
|
186
|
+
catch { failed(codeError("workbench_change_response_invalid")); }
|
|
187
|
+
});
|
|
188
|
+
events.addEventListener("source-error", (event) => {
|
|
189
|
+
if (stopped) return;
|
|
190
|
+
sourceFailed = true;
|
|
191
|
+
let code;
|
|
192
|
+
try { code = JSON.parse(event.data)?.code; } catch { /* Keep a fixed public fallback. */ }
|
|
193
|
+
failed(codeError(publicCode(code, "workbench_change_source_failed")));
|
|
194
|
+
});
|
|
195
|
+
events.addEventListener("source-ready", () => {
|
|
196
|
+
const firstReady = !sourceReady;
|
|
197
|
+
sourceReady = true;
|
|
198
|
+
sourceFailed = false;
|
|
199
|
+
// Initial Host readiness can arrive after the first snapshot timed out,
|
|
200
|
+
// without any preceding SSE error. Deliver that recovery exactly once.
|
|
201
|
+
if (firstReady && !reconnecting && !stopped) onReconnect?.();
|
|
202
|
+
else recovered();
|
|
203
|
+
});
|
|
204
|
+
events.addEventListener("open", recovered);
|
|
205
|
+
events.addEventListener("error", () => failed(codeError("workbench_change_stream_failed")));
|
|
206
|
+
keepAliveTimer = setTimeout(keepSessionAlive, 60_000);
|
|
207
|
+
return () => {
|
|
208
|
+
stopped = true;
|
|
209
|
+
clearTimeout(keepAliveTimer);
|
|
210
|
+
events.close();
|
|
211
|
+
};
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function publicCode(value, fallback) {
|
|
217
|
+
return typeof value === "string" && (/^(?:workbench|host|neutral_[a-z_]+|hellolynk_[a-z_]+)_[a-z0-9_]{1,120}$/.test(value)
|
|
218
|
+
|| ["powershell7_missing", "credential_helper_unavailable", "agent_instance_name_invalid", "agent_instance_name_conflict",
|
|
219
|
+
"device_enrollment_name_invalid", "device_enrollment_hellolynk_id_invalid", "device_enrollment_existing_profile_requires_setup",
|
|
220
|
+
"device_enrollment_intent_conflict", "device_enrollment_authorization_expired", "device_enrollment_owner_conflict",
|
|
221
|
+
"device_enrollment_busy", "device_enrollment_request_timeout", "device_enrollment_request_failed"].includes(value)) ? value : fallback;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function codeError(code) {
|
|
225
|
+
return Object.assign(new Error(code), { code });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function privateActionGrant(value) {
|
|
229
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{31,511}$/.test(value)) {
|
|
230
|
+
const error = new Error("workbench_surface_action_grant_invalid");
|
|
231
|
+
error.code = "workbench_surface_action_grant_invalid";
|
|
232
|
+
error.outcome = "known-failed";
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
return value;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function knownPreMutationError(code) {
|
|
239
|
+
return /^(?:workbench_surface_(?:action_grant_(?:required|invalid|expired)|command_invalid|commands_not_enabled|csrf_invalid|origin_not_allowed|session_invalid)|host_app_action_grant_(?:required|invalid|expired|mismatch|revoked))$/.test(String(code || ""));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function positiveTimeout(value, fallback) {
|
|
243
|
+
if (value == null) return fallback;
|
|
244
|
+
const timeout = Number(value);
|
|
245
|
+
if (!Number.isFinite(timeout) || timeout <= 0) throw new Error("workbench_http_transport_timeout_invalid");
|
|
246
|
+
return timeout;
|
|
247
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#54605a" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
|
2
|
+
<path d="M8.2 11a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7Zm7.6-.6a2.8 2.8 0 1 0 0-5.6m-13 14.3v-1.2a5.4 5.4 0 0 1 10.8 0v1.2m1.2-6a4.5 4.5 0 0 1 6.4 4.1v1.9"/>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#54605a" stroke-width="1.65" stroke-linecap="round" stroke-linejoin="round">
|
|
2
|
+
<path d="M12 8.2a3.8 3.8 0 1 0 0 7.6 3.8 3.8 0 0 0 0-7.6Zm7.5 3.8 1.4 1-.9 2.2-1.7-.2a7.8 7.8 0 0 1-1.4 1.4l.2 1.7-2.2.9-1-1.4a8 8 0 0 1-2 0l-1 1.4-2.2-.9.2-1.7A7.8 7.8 0 0 1 7.5 15l-1.7.2-.9-2.2 1.4-1a8 8 0 0 1 0-2l-1.4-1 .9-2.2 1.7.2a7.8 7.8 0 0 1 1.4-1.4l-.2-1.7L11 3l1 1.4a8 8 0 0 1 2 0L15 3l2.2.9-.2 1.7A7.8 7.8 0 0 1 18.4 7l1.7-.2.9 2.2-1.4 1a8 8 0 0 1 0 2Z"/>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<meta name="color-scheme" content="light dark">
|
|
7
|
+
<title>HelloLynk</title>
|
|
8
|
+
<link rel="stylesheet" href="/assets/styles.css">
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div id="synthetic-banner" class="synthetic-banner" hidden role="status"></div>
|
|
12
|
+
<main id="workbench" class="workbench" aria-busy="true">
|
|
13
|
+
<nav id="app-rail" class="product-bar" aria-label="HelloLynk">
|
|
14
|
+
<a class="brand" href="#/messages" aria-label="HelloLynk">
|
|
15
|
+
<img class="brand-mark" src="/assets/hellolynk-mark.png" alt="" aria-hidden="true">
|
|
16
|
+
<span>HelloLynk</span>
|
|
17
|
+
</a>
|
|
18
|
+
<div class="primary-navigation" aria-label="主要功能">
|
|
19
|
+
<a class="rail-link is-active" href="#/messages" data-section="inbox">消息</a>
|
|
20
|
+
<a class="rail-link" href="#/contacts" data-section="contacts"><span id="contacts-nav-label">联系人</span><span id="contacts-nav-badge" class="nav-badge" hidden aria-hidden="true"></span></a>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="portal-navigation" aria-label="官网入口">
|
|
23
|
+
<button id="account-link-devices" class="portal-link" type="button" data-portal-target="devices">设备与撤销</button>
|
|
24
|
+
<button id="account-disconnect" class="portal-link" type="button" hidden>解除连接</button>
|
|
25
|
+
<button id="locale-toggle" class="portal-link" type="button">EN</button>
|
|
26
|
+
</div>
|
|
27
|
+
<div class="product-actions">
|
|
28
|
+
<div class="refresh-control">
|
|
29
|
+
<span id="refresh-hint" class="refresh-hint">没看到新消息?点刷新</span>
|
|
30
|
+
<button id="global-refresh" class="icon-button refresh-button" type="button" aria-label="刷新消息" title="刷新消息">
|
|
31
|
+
<img src="/assets/icons/refresh.svg" alt="" aria-hidden="true">
|
|
32
|
+
<span id="refresh-label" class="sr-only">刷新消息</span>
|
|
33
|
+
</button>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="connection-status" role="status" aria-live="polite">
|
|
36
|
+
<span class="host-dot" aria-hidden="true"></span>
|
|
37
|
+
<span id="host-status">正在打开</span>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</nav>
|
|
41
|
+
|
|
42
|
+
<aside id="list-pane" class="list-pane">
|
|
43
|
+
<header id="list-header" class="list-header" hidden>
|
|
44
|
+
<div id="list-heading">
|
|
45
|
+
<span id="space-label" class="eyebrow">个人空间</span>
|
|
46
|
+
<h1 id="section-title">会话</h1>
|
|
47
|
+
</div>
|
|
48
|
+
<a id="contact-request-link" class="contact-request-link" href="#/contacts/requests" aria-label="联系申请" hidden>
|
|
49
|
+
<span id="contact-request-label">联系申请</span>
|
|
50
|
+
<span id="contact-request-badge" class="nav-badge" hidden aria-hidden="true"></span>
|
|
51
|
+
</a>
|
|
52
|
+
<button id="list-primary-action" class="compact-action" type="button" hidden></button>
|
|
53
|
+
</header>
|
|
54
|
+
<div id="thread-filters" class="thread-filters" role="group" aria-label="会话筛选">
|
|
55
|
+
<button class="filter-button is-active" type="button" data-filter="all" aria-pressed="true"><span id="filter-all-label">全部</span></button>
|
|
56
|
+
<button class="filter-button" type="button" data-filter="unread" aria-pressed="false"><span id="filter-unread-label">未读</span><b id="unread-count">0</b></button>
|
|
57
|
+
<button id="history-manage" class="filter-button" type="button" aria-expanded="false" aria-controls="history-management" hidden>管理</button>
|
|
58
|
+
</div>
|
|
59
|
+
<div id="search-region" class="search-region">
|
|
60
|
+
<label for="search-input" id="search-label" class="sr-only">搜索会话和消息</label>
|
|
61
|
+
<img src="/assets/icons/search.svg" alt="" aria-hidden="true">
|
|
62
|
+
<input id="search-input" type="search" maxlength="120" autocomplete="off" placeholder="搜索聊天或消息">
|
|
63
|
+
</div>
|
|
64
|
+
<div id="history-management" class="history-management" hidden></div>
|
|
65
|
+
<div id="inbox-read-actions" class="inbox-read-actions" hidden></div>
|
|
66
|
+
<div id="list-content" class="list-content" aria-live="polite"></div>
|
|
67
|
+
</aside>
|
|
68
|
+
|
|
69
|
+
<section id="content-pane" class="content-pane" tabindex="-1">
|
|
70
|
+
<p id="connection-result-note" class="connection-result-note" role="status" hidden></p>
|
|
71
|
+
<div id="state-layer" class="state-layer" role="status">
|
|
72
|
+
<div class="state-content">
|
|
73
|
+
<div id="connection-welcome" class="connection-welcome" hidden>
|
|
74
|
+
<img src="/assets/hellolynk-mark.png" alt="" aria-hidden="true">
|
|
75
|
+
<span id="connection-eyebrow"></span>
|
|
76
|
+
</div>
|
|
77
|
+
<span class="spinner" aria-hidden="true"></span>
|
|
78
|
+
<h2 id="state-title">正在打开 HelloLynk</h2>
|
|
79
|
+
<p id="state-message">正在读取这台电脑上的消息。</p>
|
|
80
|
+
<button id="state-retry" class="primary-button" type="button" hidden>重试</button>
|
|
81
|
+
<div id="connection-guide" class="connection-guide" hidden>
|
|
82
|
+
<p id="connection-hint"></p>
|
|
83
|
+
<button id="connection-start" class="primary-button" type="button" hidden></button>
|
|
84
|
+
<p id="connection-help" class="connection-help"></p>
|
|
85
|
+
<button id="connection-key-link" class="portal-link" type="button" data-portal-target="keys" hidden></button>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
<div id="content-view" class="content-view" hidden></div>
|
|
90
|
+
</section>
|
|
91
|
+
</main>
|
|
92
|
+
<div id="account-disconnect-layer" class="global-compose-layer" hidden>
|
|
93
|
+
<section class="global-compose-dialog" role="dialog" aria-modal="true" aria-labelledby="account-disconnect-title">
|
|
94
|
+
<h2 id="account-disconnect-title">解除当前客户端的账号连接?</h2>
|
|
95
|
+
<p id="account-disconnect-copy">旧账号的历史记录保留,解除后可以重新连接账号。</p>
|
|
96
|
+
<p id="account-disconnect-status" role="status"></p>
|
|
97
|
+
<div class="form-actions">
|
|
98
|
+
<button id="account-disconnect-cancel" class="secondary-button" type="button">取消</button>
|
|
99
|
+
<button id="account-disconnect-confirm" class="primary-button" type="button">解除连接</button>
|
|
100
|
+
</div>
|
|
101
|
+
</section>
|
|
102
|
+
</div>
|
|
103
|
+
<div id="global-compose-layer" class="global-compose-layer" hidden>
|
|
104
|
+
<section class="global-compose-dialog" role="dialog" aria-modal="true" aria-labelledby="global-compose-title">
|
|
105
|
+
<h2 id="global-compose-title">新消息</h2>
|
|
106
|
+
<label for="global-contact-select" id="global-contact-label">联系人</label>
|
|
107
|
+
<select id="global-contact-select"></select>
|
|
108
|
+
<p id="global-local-ai"></p>
|
|
109
|
+
<label for="global-instance-select" id="global-instance-label">对方 AI</label>
|
|
110
|
+
<select id="global-instance-select"></select>
|
|
111
|
+
<label for="global-message-draft" id="global-message-label">消息</label>
|
|
112
|
+
<textarea id="global-message-draft" maxlength="20480" rows="5"></textarea>
|
|
113
|
+
<div id="global-message-preview" hidden></div>
|
|
114
|
+
<p id="global-compose-status" class="composer-status" role="status" aria-live="polite"></p>
|
|
115
|
+
<div class="global-compose-actions">
|
|
116
|
+
<button id="global-compose-cancel" class="secondary-button" type="button">取消</button>
|
|
117
|
+
<button id="global-compose-submit" class="primary-button" type="button">发送</button>
|
|
118
|
+
</div>
|
|
119
|
+
</section>
|
|
120
|
+
</div>
|
|
121
|
+
<div id="local-history-layer" class="global-compose-layer" hidden>
|
|
122
|
+
<section class="global-compose-dialog" role="alertdialog" aria-modal="true" aria-labelledby="local-history-title" aria-describedby="local-history-copy">
|
|
123
|
+
<h2 id="local-history-title"></h2>
|
|
124
|
+
<p id="local-history-copy"></p>
|
|
125
|
+
<label id="local-history-range-label" for="local-history-range" hidden></label>
|
|
126
|
+
<select id="local-history-range" hidden></select>
|
|
127
|
+
<label id="local-history-date-label" for="local-history-date" hidden></label>
|
|
128
|
+
<input id="local-history-date" type="date" hidden>
|
|
129
|
+
<p id="local-history-estimate" aria-live="polite"></p>
|
|
130
|
+
<div id="local-history-status" role="status" aria-live="polite"></div>
|
|
131
|
+
<div class="form-actions">
|
|
132
|
+
<button id="local-history-cancel" class="secondary-button" type="button">取消</button>
|
|
133
|
+
<button id="local-history-confirm" class="secondary-button danger-button" type="button">清空本机记录</button>
|
|
134
|
+
</div>
|
|
135
|
+
</section>
|
|
136
|
+
</div>
|
|
137
|
+
<script type="module" src="/assets/loopback-entry.mjs"></script>
|
|
138
|
+
</body>
|
|
139
|
+
</html>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { mountWorkbench, publicError } from "/assets/app.mjs";
|
|
2
|
+
import { createWorkbenchHttpTransport } from "/assets/http-transport.mjs";
|
|
3
|
+
|
|
4
|
+
const PORTAL_URLS = Object.freeze({
|
|
5
|
+
keys: "https://hellolynk.com/account#keys",
|
|
6
|
+
devices: "https://hellolynk.com/account#devices",
|
|
7
|
+
});
|
|
8
|
+
const transport = createWorkbenchHttpTransport();
|
|
9
|
+
const nativeAccountLinks = document.body.dataset.nativeAccountLinks === "true";
|
|
10
|
+
|
|
11
|
+
function openPortal(target) {
|
|
12
|
+
const url = PORTAL_URLS[target];
|
|
13
|
+
if (!url) throw new Error("workbench_portal_target_invalid");
|
|
14
|
+
if (nativeAccountLinks) return transport.openAccount(url);
|
|
15
|
+
window.open(url, "_blank", "noopener,noreferrer");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
mountWorkbench({ transport, openPortal,
|
|
20
|
+
...(nativeAccountLinks ? { openConnectionLink: (url) => transport.openAccount(url) } : {}),
|
|
21
|
+
});
|
|
22
|
+
} catch (error) {
|
|
23
|
+
document.getElementById("state-title").textContent = "HelloLynk 暂时打不开 / HelloLynk couldn’t open";
|
|
24
|
+
document.getElementById("state-message").textContent = `页面没能启动。请回到客户端,重新打开 HelloLynk 通讯;一直不行,请把下面的错误码告诉你的 AI。 / The page didn’t start. Go back to your client and reopen HelloLynk Messaging; if it keeps failing, tell your AI the error code below.\n${publicError(error)}`;
|
|
25
|
+
document.querySelector(".spinner")?.setAttribute("hidden", "");
|
|
26
|
+
}
|