@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,103 @@
|
|
|
1
|
+
import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { validateLocalIdentityProofRequest } from "./hellolynk-local-identity-trust.mjs";
|
|
3
|
+
|
|
4
|
+
export const LOCAL_PEER_EXCHANGE_PATH = "/host/v1/local-peer-exchange";
|
|
5
|
+
const CONTRACT = "hellolynk-local-peer-exchange-v1";
|
|
6
|
+
const fail = (code = "local_peer_exchange_invalid") => Object.assign(new Error(code), { code });
|
|
7
|
+
function exact(value, fields) {
|
|
8
|
+
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
9
|
+
|| Object.keys(value).sort().join(",") !== [...fields].sort().join(",")) throw fail();
|
|
10
|
+
}
|
|
11
|
+
function id(value) {
|
|
12
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,199}$/.test(value)) throw fail();
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
const digest = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
16
|
+
|
|
17
|
+
export function validateLocalPeerRequest(value) {
|
|
18
|
+
exact(value, ["challenge", "owner", "target", "source", "action", "payload"]);
|
|
19
|
+
const base = validateLocalIdentityProofRequest({ challenge: value.challenge, owner: value.owner, target: value.target });
|
|
20
|
+
const source = validateLocalIdentityProofRequest({ ...base, target: value.source }).target;
|
|
21
|
+
if (source.hostInstallationId === base.target.hostInstallationId || source.delegateId === base.target.delegateId) throw fail();
|
|
22
|
+
if (value.action === "claim") {
|
|
23
|
+
exact(value.payload, ["routeKind", "clientMessageId", "recipientDeviceId",
|
|
24
|
+
...(value.payload?.nativeContentVersion == null ? [] : ["nativeContentVersion"])]);
|
|
25
|
+
if (value.payload.nativeContentVersion != null && value.payload.nativeContentVersion !== 1) throw fail();
|
|
26
|
+
if (!["owned_delegate", "instance_direct"].includes(value.payload.routeKind)) throw fail();
|
|
27
|
+
id(value.payload.clientMessageId);
|
|
28
|
+
if (value.payload.recipientDeviceId !== null) id(value.payload.recipientDeviceId);
|
|
29
|
+
} else if (value.action === "deliver") {
|
|
30
|
+
exact(value.payload, ["event", ...(value.payload?.nativeContentVersion == null ? [] : ["nativeContentVersion"])]);
|
|
31
|
+
if (value.payload.nativeContentVersion != null && value.payload.nativeContentVersion !== 1) throw fail();
|
|
32
|
+
const event = value.payload.event;
|
|
33
|
+
if (!event || event.schemaVersion !== 2 || !event.e2eeEnvelope || typeof event.envelopeDigest !== "string") throw fail();
|
|
34
|
+
if (event.eventId !== `same_host_${createHash("sha256").update(`${event.clientMessageId}\0${event.envelopeDigest}`).digest("hex").slice(0, 32)}`) throw fail();
|
|
35
|
+
// The full envelope/signature and all outer bindings are validated by the
|
|
36
|
+
// ordinary E2EE receiver; this boundary only fixes the selected local pair.
|
|
37
|
+
if (event.senderPrincipalId !== base.owner.principalId || event.recipientPrincipalId !== base.owner.principalId
|
|
38
|
+
|| event.senderHelloLynkId !== base.owner.hellolynkId || event.recipientHelloLynkId !== base.owner.hellolynkId
|
|
39
|
+
|| event.senderHostId !== source.hostInstallationId || event.senderDelegateId !== source.delegateId
|
|
40
|
+
|| event.senderDelegateAuthorizationVersion !== source.authorizationVersion
|
|
41
|
+
|| event.recipientHostId !== base.target.hostInstallationId || event.recipientDelegateId !== base.target.delegateId
|
|
42
|
+
|| event.recipientDelegateAuthorizationVersion !== base.target.authorizationVersion
|
|
43
|
+
|| !["owned_delegate", "instance_direct"].includes(event.routeKind)) throw fail();
|
|
44
|
+
} else throw fail();
|
|
45
|
+
return { ...base, source, action: value.action, payload: value.payload };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function signedPayload({ request, hostGeneration, capabilityId, result }) {
|
|
49
|
+
return { contract: CONTRACT, requestDigest: digest(validateLocalPeerRequest(request)),
|
|
50
|
+
hostGeneration: id(hostGeneration), capabilityId: id(capabilityId), result };
|
|
51
|
+
}
|
|
52
|
+
function mac(value, secret) {
|
|
53
|
+
if (!Buffer.isBuffer(secret) || secret.length < 32) throw fail();
|
|
54
|
+
return createHmac("sha256", secret).update(`${CONTRACT}\n${JSON.stringify(value)}`).digest("hex");
|
|
55
|
+
}
|
|
56
|
+
export function signLocalPeerResult(input) {
|
|
57
|
+
const value = signedPayload(input);
|
|
58
|
+
return { ...value, proof: mac(value, input.secretBuffer) };
|
|
59
|
+
}
|
|
60
|
+
export function verifyLocalPeerResult(input) {
|
|
61
|
+
exact(input.response, ["contract", "requestDigest", "hostGeneration", "capabilityId", "result", "proof"]);
|
|
62
|
+
const expected = signedPayload({ ...input, result: input.response.result });
|
|
63
|
+
const { proof, ...actual } = input.response;
|
|
64
|
+
if (JSON.stringify(expected) !== JSON.stringify(actual) || !/^[a-f0-9]{64}$/.test(proof)
|
|
65
|
+
|| !timingSafeEqual(Buffer.from(proof, "hex"), Buffer.from(mac(expected, input.secretBuffer), "hex"))) throw fail();
|
|
66
|
+
return actual.result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function createLocalPeerTransport({ profile, access }) {
|
|
70
|
+
const source = { hostInstallationId: profile.hostId, delegateId: profile.delegateId,
|
|
71
|
+
authorizationVersion: profile.authorizationVersion };
|
|
72
|
+
async function exchange(target, action, payload, { required, signal } = {}) {
|
|
73
|
+
return access(target, async (peer) => {
|
|
74
|
+
if (!peer.binding.capability.scopes.includes("app-data:authorized-messaging")) throw fail("local_peer_messaging_unavailable");
|
|
75
|
+
const request = validateLocalPeerRequest({ challenge: randomBytes(32).toString("hex"), owner: peer.owner,
|
|
76
|
+
target: { hostInstallationId: peer.binding.hostInstallationId, delegateId: peer.binding.delegateId,
|
|
77
|
+
authorizationVersion: peer.target.authorizationVersion }, source, action, payload });
|
|
78
|
+
const response = await peer.request(LOCAL_PEER_EXCHANGE_PATH, request);
|
|
79
|
+
// No transport switch after this call, including timeout/invalid ACK.
|
|
80
|
+
// The messenger retains its existing outcome-unknown and retry rules.
|
|
81
|
+
if (response.status !== 200) throw fail("local_peer_exchange_unconfirmed");
|
|
82
|
+
return verifyLocalPeerResult({ response: response.body, request, hostGeneration: peer.discovery.hostGeneration,
|
|
83
|
+
capabilityId: peer.binding.capability.capabilityId, secretBuffer: peer.binding.capability.secretBuffer });
|
|
84
|
+
}, { required, signal, proveIdentity: false, timeoutMs: 30_000 });
|
|
85
|
+
}
|
|
86
|
+
return Object.freeze({
|
|
87
|
+
claim(input, { signal } = {}) {
|
|
88
|
+
return exchange({ peerHelloLynkId: profile.hellolynkId, peerDelegateId: input.recipientDelegateId,
|
|
89
|
+
peerHostId: input.recipientHostId }, "claim", { routeKind: input.routeKind, clientMessageId: input.clientMessageId,
|
|
90
|
+
recipientDeviceId: input.recipientDeviceId ?? null,
|
|
91
|
+
...(input.nativeContentVersion ? { nativeContentVersion: input.nativeContentVersion } : {}) }, { signal });
|
|
92
|
+
},
|
|
93
|
+
async deliver(event, nativeContentVersion = null) {
|
|
94
|
+
const result = await exchange({ peerHelloLynkId: event.recipientHelloLynkId, peerDelegateId: event.recipientDelegateId,
|
|
95
|
+
peerHostId: event.recipientHostId, peerAuthorizationVersion: event.recipientDelegateAuthorizationVersion },
|
|
96
|
+
"deliver", { event, ...(nativeContentVersion == null ? {} : { nativeContentVersion }) }, { required: true });
|
|
97
|
+
exact(result, ["acknowledged", "duplicate", "replyTargetDeleted", "eventId", "envelopeDigest"]);
|
|
98
|
+
if (result.acknowledged !== true || typeof result.duplicate !== "boolean" || typeof result.replyTargetDeleted !== "boolean"
|
|
99
|
+
|| result.eventId !== event.eventId || result.envelopeDigest !== event.envelopeDigest) throw fail("local_peer_ack_invalid");
|
|
100
|
+
return result;
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import net from "node:net";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { performance } from "node:perf_hooks";
|
|
7
|
+
|
|
8
|
+
// Serialize lock-file changes with an OS-owned resource. A check followed by
|
|
9
|
+
// rename/unlink is not a compare-and-swap, even when the file is checked twice.
|
|
10
|
+
// The OS releases this guard if its process exits; there is no second stale
|
|
11
|
+
// lock file to reclaim. Nothing is read from or written to connected clients.
|
|
12
|
+
export async function withLocalProcessLock(target, operation, { timeoutMs = 10_000 } = {}) {
|
|
13
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || typeof operation !== "function") {
|
|
14
|
+
throw lockError("hellolynk_local_process_lock_invalid");
|
|
15
|
+
}
|
|
16
|
+
const started = performance.now();
|
|
17
|
+
const parent = await fs.realpath(path.dirname(target));
|
|
18
|
+
const canonical = path.join(parent, path.basename(target));
|
|
19
|
+
const scope = process.platform === "win32" || process.platform === "darwin"
|
|
20
|
+
? canonical.toLowerCase() : canonical;
|
|
21
|
+
const digest = crypto.createHash("sha256").update(scope).digest("hex");
|
|
22
|
+
const address = process.platform === "win32"
|
|
23
|
+
? { path: `\\\\.\\pipe\\hellolynk-file-lock-${digest}` }
|
|
24
|
+
: process.platform === "linux"
|
|
25
|
+
? { path: `\0hellolynk-file-lock-${digest}` }
|
|
26
|
+
// macOS has no abstract Unix sockets. A deterministic loopback port is
|
|
27
|
+
// also released on crash. A collision fails closed; never select another
|
|
28
|
+
// port, which would let contenders for the same file use different locks.
|
|
29
|
+
: { host: "127.0.0.1", port: 16_384 + (Number.parseInt(digest.slice(0, 8), 16) % 49_152) };
|
|
30
|
+
let server;
|
|
31
|
+
for (;;) {
|
|
32
|
+
if (performance.now() - started >= timeoutMs) throw lockError("hellolynk_local_process_lock_busy");
|
|
33
|
+
server = net.createServer((socket) => { socket.on("error", () => {}); socket.destroy(); });
|
|
34
|
+
try {
|
|
35
|
+
await new Promise((resolve, reject) => {
|
|
36
|
+
server.once("error", reject);
|
|
37
|
+
server.listen({ ...address, exclusive: true }, resolve);
|
|
38
|
+
});
|
|
39
|
+
break;
|
|
40
|
+
} catch (error) {
|
|
41
|
+
server.close();
|
|
42
|
+
if (error?.code !== "EADDRINUSE") throw lockError("hellolynk_local_process_lock_unavailable", error);
|
|
43
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(25, Math.max(1, timeoutMs - (performance.now() - started)))));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
try { return await operation(); }
|
|
47
|
+
finally { await new Promise((resolve) => server.close(resolve)); }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function lockError(code, cause) { return Object.assign(new Error(code, cause ? { cause } : undefined), { code }); }
|
|
51
|
+
|
|
52
|
+
// Process birth identity distinguishes an active lock owner from a reused PID.
|
|
53
|
+
const SELF_PROCESS_STARTED_AT_MS = Date.now() - (process.uptime() * 1_000);
|
|
54
|
+
let selfProcessIdentityPromise = null;
|
|
55
|
+
|
|
56
|
+
export async function inspectProcessIdentity(pid) {
|
|
57
|
+
if (!Number.isSafeInteger(pid) || pid < 1) return null;
|
|
58
|
+
if (pid === process.pid) {
|
|
59
|
+
selfProcessIdentityPromise ||= inspectPlatformProcessIdentity(pid).then((value) => value || Object.freeze({
|
|
60
|
+
identity: `portable:${process.platform}:${Math.round(SELF_PROCESS_STARTED_AT_MS)}`,
|
|
61
|
+
startedAtMs: SELF_PROCESS_STARTED_AT_MS,
|
|
62
|
+
}));
|
|
63
|
+
return selfProcessIdentityPromise;
|
|
64
|
+
}
|
|
65
|
+
return inspectPlatformProcessIdentity(pid);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function inspectPlatformProcessIdentity(pid) {
|
|
69
|
+
if (process.platform === "linux") return inspectLinuxProcessIdentity(pid);
|
|
70
|
+
if (process.platform === "win32") return inspectWindowsProcessIdentity(pid);
|
|
71
|
+
if (process.platform === "darwin") return inspectDarwinProcessIdentity(pid);
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function inspectLinuxProcessIdentity(pid) {
|
|
76
|
+
try {
|
|
77
|
+
const [statText, bootId, processMetadata] = await Promise.all([
|
|
78
|
+
fs.readFile(`/proc/${pid}/stat`, "utf8"),
|
|
79
|
+
fs.readFile("/proc/sys/kernel/random/boot_id", "utf8"),
|
|
80
|
+
fs.stat(`/proc/${pid}`),
|
|
81
|
+
]);
|
|
82
|
+
const close = statText.lastIndexOf(")");
|
|
83
|
+
if (close < 1) return null;
|
|
84
|
+
const fields = statText.slice(close + 2).trim().split(/\s+/);
|
|
85
|
+
const startTicks = fields[19];
|
|
86
|
+
const normalizedBootId = bootId.trim().toLowerCase();
|
|
87
|
+
if (!/^\d+$/.test(String(startTicks || ""))
|
|
88
|
+
|| !/^[a-f0-9-]{32,40}$/.test(normalizedBootId)) return null;
|
|
89
|
+
return Object.freeze({
|
|
90
|
+
identity: `linux:${normalizedBootId}:${startTicks}`,
|
|
91
|
+
startedAtMs: Number.isFinite(processMetadata.ctimeMs) ? processMetadata.ctimeMs : null,
|
|
92
|
+
});
|
|
93
|
+
} catch { return null; }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function inspectWindowsProcessIdentity(pid) {
|
|
97
|
+
const systemRoot = String(process.env.SystemRoot || "C:\\Windows");
|
|
98
|
+
if (!path.isAbsolute(systemRoot)) return null;
|
|
99
|
+
const powershell = path.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
100
|
+
const script = `$p=Get-Process -Id ${pid} -ErrorAction Stop;[Console]::Out.Write($p.StartTime.ToUniversalTime().Ticks.ToString())`;
|
|
101
|
+
const output = await runIdentityProbe(powershell, [
|
|
102
|
+
"-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script,
|
|
103
|
+
]);
|
|
104
|
+
if (!/^\d{12,20}$/.test(String(output || ""))) return null;
|
|
105
|
+
const ticks = Number(output);
|
|
106
|
+
const startedAtMs = (ticks / 10_000) - 62_135_596_800_000;
|
|
107
|
+
if (!Number.isFinite(startedAtMs) || startedAtMs < 0) return null;
|
|
108
|
+
return Object.freeze({ identity: `win32:${output}`, startedAtMs });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function inspectDarwinProcessIdentity(pid) {
|
|
112
|
+
const output = await runIdentityProbe("/bin/ps", ["-o", "lstart=", "-p", String(pid)]);
|
|
113
|
+
const startedAtMs = Date.parse(String(output || "").trim());
|
|
114
|
+
if (!Number.isFinite(startedAtMs)) return null;
|
|
115
|
+
return Object.freeze({
|
|
116
|
+
identity: `darwin:${crypto.createHash("sha256").update(output.trim()).digest("hex")}`,
|
|
117
|
+
startedAtMs,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function runIdentityProbe(executable, args) {
|
|
122
|
+
return new Promise((resolve) => {
|
|
123
|
+
let output = "";
|
|
124
|
+
let overflow = false;
|
|
125
|
+
let settled = false;
|
|
126
|
+
let child;
|
|
127
|
+
const settle = (value) => {
|
|
128
|
+
if (settled) return;
|
|
129
|
+
settled = true;
|
|
130
|
+
clearTimeout(timer);
|
|
131
|
+
resolve(value);
|
|
132
|
+
};
|
|
133
|
+
const timer = setTimeout(() => {
|
|
134
|
+
child?.kill();
|
|
135
|
+
settle(null);
|
|
136
|
+
}, 2_000);
|
|
137
|
+
try {
|
|
138
|
+
child = spawn(executable, args, {
|
|
139
|
+
windowsHide: true,
|
|
140
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
141
|
+
});
|
|
142
|
+
} catch {
|
|
143
|
+
settle(null);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
child.stdout.on("data", (chunk) => {
|
|
147
|
+
if (overflow) return;
|
|
148
|
+
output += chunk.toString("utf8");
|
|
149
|
+
if (Buffer.byteLength(output, "utf8") > 256) {
|
|
150
|
+
output = "";
|
|
151
|
+
overflow = true;
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
child.once("error", () => settle(null));
|
|
155
|
+
child.once("close", (code) => settle(code === 0 && !overflow ? output.trim() : null));
|
|
156
|
+
});
|
|
157
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { readNativeReplyRoute } from "./hellolynk-native-reply-routes.mjs";
|
|
2
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
3
|
+
import { readNativeInboxBatchDispatch } from "./hellolynk-native-inbox-runtime.mjs";
|
|
4
|
+
|
|
5
|
+
const ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,240}$/;
|
|
6
|
+
const READS = new Set(["list", "read", "pending", "count"]);
|
|
7
|
+
const fail = code => { throw Object.assign(new Error(code), { code }); };
|
|
8
|
+
export function modelInboxCaller(value) {
|
|
9
|
+
if (!value || typeof value !== "object" || !ID.test(value.threadId || "")) fail("model_inbox_context_unavailable");
|
|
10
|
+
// Aliases are supplied by a verified native bridge, never by tool arguments.
|
|
11
|
+
if (value.aliases != null && (!Array.isArray(value.aliases) || value.aliases.length > 2
|
|
12
|
+
|| value.aliases.some(id => !ID.test(id)))) fail("model_inbox_context_unavailable");
|
|
13
|
+
return { threadId: value.threadId, ...(value.aliases?.length ? { aliases: [...new Set(value.aliases)] } : {}) };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Request-local only. The dispatcher keeps its original, unscoped operations.
|
|
17
|
+
export async function runModelInbox(request, io) {
|
|
18
|
+
const { operation, query = {}, scope = "current" } = request;
|
|
19
|
+
if (![...READS, "claim", "assertReply"].includes(operation)
|
|
20
|
+
|| scope !== "current") fail("model_inbox_scope_invalid");
|
|
21
|
+
const caller = modelInboxCaller(request.caller);
|
|
22
|
+
const nativeIds = new Set([caller.threadId, ...(caller.aliases || [])]);
|
|
23
|
+
const assignmentCache = new Map(), messageAssignments = new Map();
|
|
24
|
+
async function preparePage(messages) {
|
|
25
|
+
if (!messages.length) return;
|
|
26
|
+
const rows = await io.assignments(messages.map(message => message.localMessageId));
|
|
27
|
+
for (const message of messages) messageAssignments.set(message.localMessageId, rows[message.localMessageId] ?? null);
|
|
28
|
+
}
|
|
29
|
+
const nativeThread = async assignment => {
|
|
30
|
+
if (!assignment) return null;
|
|
31
|
+
if (assignment.nativeThreadId) return assignment.nativeThreadId;
|
|
32
|
+
if (!assignmentCache.has(assignment.batchId)) {
|
|
33
|
+
assignmentCache.set(assignment.batchId, (await readNativeInboxBatchDispatch(io.nativeScope, assignment.batchId))?.threadId || null);
|
|
34
|
+
}
|
|
35
|
+
return assignmentCache.get(assignment.batchId);
|
|
36
|
+
};
|
|
37
|
+
const targetThread = target => {
|
|
38
|
+
const prefix = io.nativeScope.adapterId === "claude-code" ? "claude-original:" : io.nativeScope.adapterId + "-origin:";
|
|
39
|
+
return typeof target === "string" && target.startsWith(prefix) ? target.slice(prefix.length) : null;
|
|
40
|
+
};
|
|
41
|
+
async function assertAssignment(assignment) {
|
|
42
|
+
if (!assignment) fail("model_inbox_unassigned");
|
|
43
|
+
const deadline = Date.now() + 5_000;
|
|
44
|
+
while (true) {
|
|
45
|
+
const owner = assignment.nativeThreadId || targetThread(assignment.target);
|
|
46
|
+
if (owner) {
|
|
47
|
+
if (!nativeIds.has(owner)) fail("model_inbox_other_conversation");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const dispatch = await readNativeInboxBatchDispatch(io.nativeScope, assignment.batchId);
|
|
51
|
+
if (dispatch?.threadId) {
|
|
52
|
+
if (!nativeIds.has(dispatch.threadId)) fail("model_inbox_other_conversation");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
// Codex creates and prompts atomically: its first claim can precede the
|
|
56
|
+
// create response. Wait only for the authoritative owner, never guess it.
|
|
57
|
+
if (!dispatch?.pending) {
|
|
58
|
+
// Confirm can pin the owner and remove runtime state between our reads.
|
|
59
|
+
const latest = await io.assignment({ batchId: assignment.batchId });
|
|
60
|
+
if (latest?.nativeThreadId) { assignment = latest; continue; }
|
|
61
|
+
fail("model_inbox_unassigned");
|
|
62
|
+
}
|
|
63
|
+
if (Date.now() >= deadline) fail("model_inbox_owner_pending");
|
|
64
|
+
await delay(100);
|
|
65
|
+
assignment = await io.assignment({ batchId: assignment.batchId });
|
|
66
|
+
if (!assignment) fail("model_inbox_unassigned");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async function owns(message, assert = false) {
|
|
70
|
+
const assignment = messageAssignments.has(message.localMessageId) ? messageAssignments.get(message.localMessageId)
|
|
71
|
+
: await io.assignment({ localMessageId: message.localMessageId });
|
|
72
|
+
if (assignment) {
|
|
73
|
+
if (assert) { await assertAssignment(assignment); return true; }
|
|
74
|
+
const owner = await nativeThread(assignment);
|
|
75
|
+
if (owner) return nativeIds.has(owner);
|
|
76
|
+
// A known original target is immutable. Never infer historical ownership
|
|
77
|
+
// from a carrier that may since have been closed and recreated.
|
|
78
|
+
return nativeIds.has(targetThread(assignment.target));
|
|
79
|
+
}
|
|
80
|
+
if (message.targetConversationId && message.replyToClientMessageId) fail("model_inbox_route_invalid");
|
|
81
|
+
if (message.targetConversationId) return nativeIds.has(message.targetConversationId);
|
|
82
|
+
if (message.replyToClientMessageId) {
|
|
83
|
+
return nativeIds.has((await readNativeReplyRoute(io.nativeScope, message))?.threadId);
|
|
84
|
+
}
|
|
85
|
+
if (assert) fail("model_inbox_unassigned");
|
|
86
|
+
return false; // New, unassigned mail belongs to the backend dispatcher.
|
|
87
|
+
}
|
|
88
|
+
async function assertMessage() {
|
|
89
|
+
const message = await io.metadata(query);
|
|
90
|
+
if (!await owns(message, true)) fail("model_inbox_other_conversation");
|
|
91
|
+
return message;
|
|
92
|
+
}
|
|
93
|
+
async function history({ count = false } = {}) {
|
|
94
|
+
const selected = []; let unread = 0, before = query.before;
|
|
95
|
+
while (true) {
|
|
96
|
+
const page = await io.list(before == null ? {} : { before });
|
|
97
|
+
if (!Array.isArray(page.messages)) fail("model_inbox_response_invalid");
|
|
98
|
+
await preparePage(page.messages);
|
|
99
|
+
for (const message of page.messages) {
|
|
100
|
+
if (await owns(message)) {
|
|
101
|
+
if (count) unread += Number(message.unread === true);
|
|
102
|
+
else selected.push(message);
|
|
103
|
+
}
|
|
104
|
+
if (!count && selected.length === 10) return { messages: selected };
|
|
105
|
+
}
|
|
106
|
+
if (!page.messages.length) return count ? unread : { messages: selected };
|
|
107
|
+
const next = page.messages.at(-1).localMessageId;
|
|
108
|
+
if (!next || next === before) fail("model_inbox_cursor_invalid");
|
|
109
|
+
before = next;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function pending({ count = false } = {}) {
|
|
113
|
+
const selected = []; let total = 0, cursor = query.cursor ?? 0;
|
|
114
|
+
while (true) {
|
|
115
|
+
const page = await io.pending({ cursor });
|
|
116
|
+
if (!Array.isArray(page.messages)) fail("model_inbox_response_invalid");
|
|
117
|
+
await preparePage(page.messages);
|
|
118
|
+
for (const message of page.messages) if (await owns(message)) {
|
|
119
|
+
if (count) total++;
|
|
120
|
+
else {
|
|
121
|
+
selected.push(message);
|
|
122
|
+
if (selected.length === 1000) return { messages: selected, nextCursor: message.receiptSeq };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (page.nextCursor == null) return count ? total : { messages: selected, nextCursor: null };
|
|
126
|
+
if (!Number.isSafeInteger(page.nextCursor) || page.nextCursor <= cursor) fail("model_inbox_cursor_invalid");
|
|
127
|
+
cursor = page.nextCursor;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (operation === "list") return history();
|
|
131
|
+
if (operation === "pending") return pending();
|
|
132
|
+
if (operation === "count") {
|
|
133
|
+
const humanUnreadCount = await history({ count: true });
|
|
134
|
+
const aiPendingCount = await pending({ count: true });
|
|
135
|
+
return { unreadCount: humanUnreadCount, humanUnreadCount, aiPendingCount, scope };
|
|
136
|
+
}
|
|
137
|
+
if (operation === "read") {
|
|
138
|
+
await assertMessage();
|
|
139
|
+
return io.read(query);
|
|
140
|
+
}
|
|
141
|
+
if (operation === "claim") {
|
|
142
|
+
if (query.batchId) {
|
|
143
|
+
const assignment = await io.assignment({ batchId: query.batchId });
|
|
144
|
+
await assertAssignment(assignment);
|
|
145
|
+
} else await assertMessage();
|
|
146
|
+
return io.claim(query);
|
|
147
|
+
}
|
|
148
|
+
await assertMessage(); // Before reply-origin registration as well as send.
|
|
149
|
+
return { allowed: true };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const MODEL_TOOLS = Object.freeze({
|
|
153
|
+
hellolynk_get_unread_count: "count", hellolynk_list_authorized_inbox_messages: "list",
|
|
154
|
+
hellolynk_read_authorized_inbox_message: "read", hellolynk_list_pending_messages: "pending",
|
|
155
|
+
hellolynk_claim_message: "claim", hellolynk_reply_authorized_message: "reply",
|
|
156
|
+
});
|
|
157
|
+
export const isModelInboxTool = name => Object.hasOwn(MODEL_TOOLS, name);
|
|
158
|
+
export async function prepareModelInboxInvocation(adapter, { name, args = {}, caller, readEnvelope = false, detailTarget = null }) {
|
|
159
|
+
const operation = MODEL_TOOLS[name];
|
|
160
|
+
if (!operation) return { adapter, args, finish: value => value };
|
|
161
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) fail("model_inbox_scope_invalid");
|
|
162
|
+
if (Object.hasOwn(args, "scope")) fail("model_inbox_scope_invalid");
|
|
163
|
+
const checkedCaller = modelInboxCaller(caller);
|
|
164
|
+
if (typeof adapter?.modelInbox !== "function") fail("model_inbox_unavailable");
|
|
165
|
+
const clean = { ...args };
|
|
166
|
+
const cursor = operation === "pending" ? clean.cursor : undefined;
|
|
167
|
+
if (operation === "pending") {
|
|
168
|
+
if (cursor != null && (!Number.isSafeInteger(cursor) || cursor < 0)) fail("model_inbox_scope_invalid");
|
|
169
|
+
delete clean.cursor;
|
|
170
|
+
}
|
|
171
|
+
let last;
|
|
172
|
+
const call = async (op, query = {}) => {
|
|
173
|
+
last = await adapter.modelInbox({ operation: op, scope: "current", caller: checkedCaller, query });
|
|
174
|
+
return last;
|
|
175
|
+
};
|
|
176
|
+
let assertedReply = null;
|
|
177
|
+
async function assertReply(query) {
|
|
178
|
+
const key = JSON.stringify([query.localMessageId, query.conversationKey]);
|
|
179
|
+
if (assertedReply === key) return;
|
|
180
|
+
await call('assertReply', { localMessageId: query.localMessageId, conversationKey: query.conversationKey });
|
|
181
|
+
assertedReply = key;
|
|
182
|
+
}
|
|
183
|
+
const scoped = { ...adapter,
|
|
184
|
+
assertModelInboxReply: assertReply,
|
|
185
|
+
replyAuthorizedMessage: async query => { await assertReply(query); return adapter.replyAuthorizedMessage(query); },
|
|
186
|
+
readUnreadCount: () => call("count"),
|
|
187
|
+
listAuthorizedInboxMessages: (query = {}) => call("list", query),
|
|
188
|
+
readAuthorizedInboxMessage: async (localMessageId, conversationKey) => {
|
|
189
|
+
const result = await call("read", { localMessageId, conversationKey });
|
|
190
|
+
return readEnvelope ? result : result.message;
|
|
191
|
+
},
|
|
192
|
+
listPendingMessages: (query = {}) => call("pending", { ...(query || {}), ...(cursor == null ? {} : { cursor }) }),
|
|
193
|
+
listPendingAuthorizedInboxMessages: () => call("pending", cursor == null ? {} : { cursor }),
|
|
194
|
+
claimMessage: (localMessageId, conversationKey) => call("claim", { localMessageId, conversationKey }),
|
|
195
|
+
claimAuthorizedInboxMessage: (localMessageId, conversationKey) => call("claim", { localMessageId, conversationKey }),
|
|
196
|
+
claimBatch: batchId => call("claim", { batchId }),
|
|
197
|
+
claimInboxBatch: batchId => call("claim", { batchId }),
|
|
198
|
+
};
|
|
199
|
+
function finish(value) {
|
|
200
|
+
if (!READS.has(operation)) return value;
|
|
201
|
+
if (!value || value.isError || value.ok === false || value.structuredContent?.ok === false) return value;
|
|
202
|
+
const detail = { scope: "current",
|
|
203
|
+
...(last?.nextCursor != null ? { nextCursor: last.nextCursor } : {}) };
|
|
204
|
+
if (operation === "count" && last) Object.assign(detail, {
|
|
205
|
+
humanUnreadCount: last.humanUnreadCount, aiPendingCount: last.aiPendingCount,
|
|
206
|
+
});
|
|
207
|
+
if (value.structuredContent) {
|
|
208
|
+
const structuredContent = detailTarget && value.structuredContent[detailTarget]
|
|
209
|
+
? { ...value.structuredContent, [detailTarget]: { ...value.structuredContent[detailTarget], ...detail } }
|
|
210
|
+
: { ...value.structuredContent, ...detail };
|
|
211
|
+
const originalJson = JSON.stringify(value.structuredContent), projected = JSON.stringify(structuredContent);
|
|
212
|
+
let hasProjection = false;
|
|
213
|
+
const content = (value.content || []).map(item => {
|
|
214
|
+
if (item.type !== 'text') return item;
|
|
215
|
+
try {
|
|
216
|
+
if (JSON.stringify(JSON.parse(item.text)) === originalJson) { hasProjection = true; return { ...item, text: projected }; }
|
|
217
|
+
} catch { /* Ordinary result text. */ }
|
|
218
|
+
return item;
|
|
219
|
+
});
|
|
220
|
+
if (operation === 'count') return { ...value, structuredContent, content: [
|
|
221
|
+
{ type: 'text', text: '当前对话:人类未读 ' + last.humanUnreadCount + ' 条;AI 待处理 ' + last.aiPendingCount + ' 条。' },
|
|
222
|
+
{ type: 'text', text: projected },
|
|
223
|
+
] };
|
|
224
|
+
if (!hasProjection) content.push({ type: 'text', text: projected });
|
|
225
|
+
return { ...value, structuredContent, content };
|
|
226
|
+
}
|
|
227
|
+
return { ...value, ...detail };
|
|
228
|
+
}
|
|
229
|
+
return { adapter: scoped, args: clean, finish };
|
|
230
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { AsyncLocalStorage } = require("node:async_hooks");
|
|
4
|
+
// Only collect responses belonging to this model tool invocation. Card refreshes,
|
|
5
|
+
// background polling and another account's concurrent call cannot supply notices.
|
|
6
|
+
const calls = new AsyncLocalStorage();
|
|
7
|
+
|
|
8
|
+
function captureHelloLynkModelNotices(response) {
|
|
9
|
+
const call = calls.getStore();
|
|
10
|
+
const state = response?.body?.modelNotices;
|
|
11
|
+
if (call && state && typeof state === "object") {
|
|
12
|
+
call.notices = [];
|
|
13
|
+
if (Number.isSafeInteger(state.pendingContactRequests) && state.pendingContactRequests > 0) {
|
|
14
|
+
call.notices.push(`有 ${state.pendingContactRequests} 个联系申请等你确认。`);
|
|
15
|
+
}
|
|
16
|
+
if (state.localStorageFull === true) call.notices.push("本机存储已满,已暂停接收新消息。");
|
|
17
|
+
}
|
|
18
|
+
return response;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function captureHelloLynkNativeDeliveryStatus(state) {
|
|
22
|
+
const call = calls.getStore();
|
|
23
|
+
if (!call) return;
|
|
24
|
+
call.nativeNotices = [];
|
|
25
|
+
if (state?.failure && /^[A-Za-z0-9_:-]{1,100}$/.test(state.failure.code)) {
|
|
26
|
+
call.nativeNotices.push(`本机 AI 收信提醒检查失败(${state.failure.code}),与本次发信结果无关。`);
|
|
27
|
+
}
|
|
28
|
+
if (Number.isSafeInteger(state?.unknownCount) && state.unknownCount > 0) {
|
|
29
|
+
call.nativeNotices.push(`本机 AI 收信提醒:${state.unknownCount} 条近期提醒结果未知;与本次发信结果、对方未读无关,请勿据此重发。`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function appendNotices(result, notices) {
|
|
34
|
+
if (!notices?.length) return result;
|
|
35
|
+
const text = `HelloLynk:${notices.join(" ")}`;
|
|
36
|
+
if (typeof result === "string") {
|
|
37
|
+
try {
|
|
38
|
+
const value = JSON.parse(result);
|
|
39
|
+
return JSON.stringify({ ...value, notices });
|
|
40
|
+
} catch { return `${result}\n${text}`; }
|
|
41
|
+
}
|
|
42
|
+
if (result?.result) return { ...result, result: appendNotices(result.result, notices) };
|
|
43
|
+
if (!Array.isArray(result?.content)) return result;
|
|
44
|
+
return { ...result, content: [...result.content, { type: "text", text }],
|
|
45
|
+
...(result.structuredContent ? { structuredContent: { ...result.structuredContent, notices } } : {}),
|
|
46
|
+
...(result.details ? { details: { ...result.details, notices } } : {}) };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function withHelloLynkModelNotices(operation) {
|
|
50
|
+
const call = {};
|
|
51
|
+
return calls.run(call, async () => {
|
|
52
|
+
const result = await operation();
|
|
53
|
+
// A notice can never turn a successful send into an error or cause a resend.
|
|
54
|
+
try { return appendNotices(result, [...(call.notices || []), ...(call.nativeNotices || [])]); } catch { return result; }
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
exports.captureHelloLynkModelNotices = captureHelloLynkModelNotices;
|
|
59
|
+
exports.captureHelloLynkNativeDeliveryStatus = captureHelloLynkNativeDeliveryStatus;
|
|
60
|
+
exports.withHelloLynkModelNotices = withHelloLynkModelNotices;
|