@dsbot-channel/p2p 0.0.0-stage → 0.7.0
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/LICENSE +21 -0
- package/assets/skill.md +17 -0
- package/cordis.patch.yml +3 -0
- package/dist/adapter.d.ts +82 -0
- package/dist/adapter.js +324 -0
- package/dist/guidance.d.ts +2 -0
- package/dist/guidance.js +2 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.js +142 -0
- package/dist/p2p-auth.d.ts +40 -0
- package/dist/p2p-auth.js +44 -0
- package/dist/p2p-dedupe.d.ts +37 -0
- package/dist/p2p-dedupe.js +107 -0
- package/dist/p2p-listener.d.ts +43 -0
- package/dist/p2p-listener.js +168 -0
- package/dist/p2p-mapping.d.ts +29 -0
- package/dist/p2p-mapping.js +80 -0
- package/dist/p2p-protocol.d.ts +94 -0
- package/dist/p2p-protocol.js +229 -0
- package/dist/p2p-secrets.d.ts +28 -0
- package/dist/p2p-secrets.js +50 -0
- package/dist/p2p-send.d.ts +37 -0
- package/dist/p2p-send.js +137 -0
- package/dist/p2p-status.d.ts +1 -0
- package/dist/p2p-status.js +1 -0
- package/dist/p2p-wire.d.ts +18 -0
- package/dist/p2p-wire.js +1 -0
- package/package.json +50 -3
- package/README.md +0 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 zerodegress
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/assets/skill.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
### p2p transport
|
|
2
|
+
|
|
3
|
+
| Field | Default | Meaning |
|
|
4
|
+
| --- | --- | --- |
|
|
5
|
+
| `listen.host` | `127.0.0.1` | concrete address only |
|
|
6
|
+
| `listen.port` | `7123` | |
|
|
7
|
+
| `peers` | required | list or mapping of peer instance to secret reference; a list entry also carries `url`, `host`, `botName` |
|
|
8
|
+
| `dedupe.ttlMs` / `dedupe.limit` | | dedupe window and size |
|
|
9
|
+
| `inflightTtlMs` | `300000` | stale claim is reclaimed after this |
|
|
10
|
+
| `sentAtWindowMs` | `300000` | freshness window for the signature |
|
|
11
|
+
| `maxBodyBytes` | | request body limit |
|
|
12
|
+
| `waitMs` | | how long a duplicate waits |
|
|
13
|
+
|
|
14
|
+
`peers[].instance` is the peer's name, not this link's — this is the one place a name is not this adapter's `channel`. Two entries of one peer instance must reference different secrets.
|
|
15
|
+
|
|
16
|
+
`@dsbot-channel/p2p` supplies this transport: mount it after `@dsbot-channel/core`, then write these fields under `transport` in this plugin's own `config`, beside `channel` and `botName`; `kind` is not written. Each entry is one instance, so the same package mounts twice with two different `channel` values. Without `@dsbot-channel/core` the plugin stays pending on `dsbot-channel.registry`.
|
|
17
|
+
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { AdapterStatus } from '@dsbot-channel/core/adapter-status.js';
|
|
2
|
+
import type { ChannelSnapshot } from '@dsbot-channel/core/channel-stats.js';
|
|
3
|
+
import type { AdapterCheckSettings, RefuseInput, RefuseResult, AdapterHandle, AdapterPorts, ChannelAdapter, InboundMessage, SendInput, SendResult } from '@dsbot-channel/core/contract.js';
|
|
4
|
+
import type { Scope } from '@dsbot-channel/core/event-types.js';
|
|
5
|
+
import type { InboundRequest } from './p2p-wire.js';
|
|
6
|
+
import type { PeerRef } from './p2p-secrets.js';
|
|
7
|
+
export declare const DEFAULT_INFLIGHT_TTL_MS = 300000;
|
|
8
|
+
/** Longest note a refusal carries on the wire, one line. */
|
|
9
|
+
export declare const REFUSE_NOTE_MAX = 200;
|
|
10
|
+
export interface P2PPeerConfig extends PeerRef {
|
|
11
|
+
url?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface P2PAdapterConfig {
|
|
14
|
+
channel: string;
|
|
15
|
+
botName: string;
|
|
16
|
+
peers: readonly P2PPeerConfig[];
|
|
17
|
+
listen?: {
|
|
18
|
+
host?: string;
|
|
19
|
+
port?: number;
|
|
20
|
+
} | undefined;
|
|
21
|
+
dedupe?: {
|
|
22
|
+
ttlMs?: number;
|
|
23
|
+
limit?: number;
|
|
24
|
+
} | undefined;
|
|
25
|
+
inflightTtlMs?: number | undefined;
|
|
26
|
+
sentAtWindowMs?: number | undefined;
|
|
27
|
+
maxBodyBytes?: number | undefined;
|
|
28
|
+
waitMs?: number | undefined;
|
|
29
|
+
status?: boolean | undefined;
|
|
30
|
+
}
|
|
31
|
+
export interface P2PAdapterOptions {
|
|
32
|
+
warn?: ((message: string) => void) | undefined;
|
|
33
|
+
now?: (() => number) | undefined;
|
|
34
|
+
}
|
|
35
|
+
/** The p2p transport behind the adapter contract: one configured channel, one listener. */
|
|
36
|
+
export declare class P2PAdapter implements ChannelAdapter {
|
|
37
|
+
#private;
|
|
38
|
+
readonly kind = "p2p";
|
|
39
|
+
readonly guidance: {
|
|
40
|
+
peer: string;
|
|
41
|
+
group: string;
|
|
42
|
+
};
|
|
43
|
+
readonly capabilities: {
|
|
44
|
+
text: boolean;
|
|
45
|
+
image: {
|
|
46
|
+
in: boolean;
|
|
47
|
+
out: boolean;
|
|
48
|
+
fromUrl: boolean;
|
|
49
|
+
fromHandle: boolean;
|
|
50
|
+
};
|
|
51
|
+
file: {
|
|
52
|
+
in: boolean;
|
|
53
|
+
out: boolean;
|
|
54
|
+
fromUrl: boolean;
|
|
55
|
+
fromHandle: boolean;
|
|
56
|
+
};
|
|
57
|
+
voice: {
|
|
58
|
+
in: boolean;
|
|
59
|
+
out: boolean;
|
|
60
|
+
fromUrl: boolean;
|
|
61
|
+
fromHandle: boolean;
|
|
62
|
+
};
|
|
63
|
+
recall: boolean;
|
|
64
|
+
history: import("@dsbot-channel/core/adapter-status.js").HistoryCapability;
|
|
65
|
+
mention: boolean;
|
|
66
|
+
reply: boolean;
|
|
67
|
+
stream: boolean;
|
|
68
|
+
};
|
|
69
|
+
constructor(config: P2PAdapterConfig, options?: P2PAdapterOptions);
|
|
70
|
+
get snapshot(): ChannelSnapshot;
|
|
71
|
+
checkConfig(settings: AdapterCheckSettings): void;
|
|
72
|
+
sessionScope(key: string): Scope | undefined;
|
|
73
|
+
checkSessionKey(key: string): string | undefined;
|
|
74
|
+
checkTarget(target: string): string | undefined;
|
|
75
|
+
describeTargets(): string;
|
|
76
|
+
status(): AdapterStatus;
|
|
77
|
+
send(input: SendInput): Promise<SendResult>;
|
|
78
|
+
refuse(input: RefuseInput): Promise<RefuseResult>;
|
|
79
|
+
start(ports: AdapterPorts): Promise<AdapterHandle>;
|
|
80
|
+
/** Maps a verified wire request onto the inbound message core takes; the adapter owns every field. */
|
|
81
|
+
toInbound(request: InboundRequest): InboundMessage;
|
|
82
|
+
}
|
package/dist/adapter.js
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { NO_HISTORY, buildListenerHealth, buildStatus } from '@dsbot-channel/core/adapter-status.js';
|
|
2
|
+
import { countersOf } from '@dsbot-channel/core/channel-stats.js';
|
|
3
|
+
import { ChannelStats } from '@dsbot-channel/core/channel-stats.js';
|
|
4
|
+
import { ConfigError } from '@dsbot-channel/core/config.js';
|
|
5
|
+
import { REPLY_TARGET_MAX, SESSION_KEY_MAX, tokenProblem } from '@dsbot-channel/core/contract.js';
|
|
6
|
+
import { GROUP_GUIDANCE, PEER_GUIDANCE } from './guidance.js';
|
|
7
|
+
import { DedupeTable } from './p2p-dedupe.js';
|
|
8
|
+
import { DEFAULT_HOST, DEFAULT_PORT, checkListenerConfig, startListener } from './p2p-listener.js';
|
|
9
|
+
import { STATUS_ROUTE } from './p2p-status.js';
|
|
10
|
+
import { cleanExcerpt, oneLine, toP2PEvent } from './p2p-mapping.js';
|
|
11
|
+
import { DECLINED, DEFAULT_SENT_AT_WINDOW_MS, MAX_BODY_BYTES } from './p2p-protocol.js';
|
|
12
|
+
import { sendToPeer } from './p2p-send.js';
|
|
13
|
+
export const DEFAULT_INFLIGHT_TTL_MS = 300_000;
|
|
14
|
+
/** Longest note a refusal carries on the wire, one line. */
|
|
15
|
+
export const REFUSE_NOTE_MAX = 200;
|
|
16
|
+
const DEFAULT_DEDUPE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
17
|
+
const DEFAULT_DEDUPE_LIMIT = 100_000;
|
|
18
|
+
function wholeMs(field, value, atLeast) {
|
|
19
|
+
if (!Number.isInteger(value) || value < atLeast) {
|
|
20
|
+
throw new ConfigError(`${field} must be a whole number of milliseconds, at least ${atLeast}: ${value}`, [
|
|
21
|
+
'adapters',
|
|
22
|
+
'p2p',
|
|
23
|
+
field,
|
|
24
|
+
]);
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
/** The p2p transport behind the adapter contract: one configured channel, one listener. */
|
|
29
|
+
export class P2PAdapter {
|
|
30
|
+
kind = 'p2p';
|
|
31
|
+
guidance = { peer: PEER_GUIDANCE, group: GROUP_GUIDANCE };
|
|
32
|
+
capabilities = {
|
|
33
|
+
text: true,
|
|
34
|
+
image: { in: true, out: true, fromUrl: true, fromHandle: false },
|
|
35
|
+
file: { in: true, out: true, fromUrl: true, fromHandle: false },
|
|
36
|
+
voice: { in: true, out: true, fromUrl: true, fromHandle: false },
|
|
37
|
+
recall: false,
|
|
38
|
+
history: NO_HISTORY,
|
|
39
|
+
mention: false,
|
|
40
|
+
reply: true,
|
|
41
|
+
stream: false,
|
|
42
|
+
};
|
|
43
|
+
#config;
|
|
44
|
+
#options;
|
|
45
|
+
#stats;
|
|
46
|
+
#now;
|
|
47
|
+
#table;
|
|
48
|
+
#ports;
|
|
49
|
+
#lastSuccessAt = null;
|
|
50
|
+
#lastSendFailure = null;
|
|
51
|
+
constructor(config, options = {}) {
|
|
52
|
+
this.#config = config;
|
|
53
|
+
this.#options = options;
|
|
54
|
+
this.#stats = new ChannelStats(options.now);
|
|
55
|
+
this.#now = options.now ?? Date.now;
|
|
56
|
+
}
|
|
57
|
+
get snapshot() {
|
|
58
|
+
return this.#stats.snapshot();
|
|
59
|
+
}
|
|
60
|
+
/** Records how one send ended: a success timestamps the link, a failure keeps the peer and the code. */
|
|
61
|
+
#settleSend(instance, result) {
|
|
62
|
+
const at = this.#now();
|
|
63
|
+
if (result.ok) {
|
|
64
|
+
this.#lastSuccessAt = at;
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
this.#stats.sendFailed(result.code);
|
|
68
|
+
this.#lastSendFailure = { at, instance, reason: result.code };
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
checkConfig(settings) {
|
|
72
|
+
if (this.#config.peers.length === 0) {
|
|
73
|
+
throw new ConfigError('at least one peer is required for each p2p adapter', ['adapters', 'p2p', 'peers']);
|
|
74
|
+
}
|
|
75
|
+
const host = this.#config.listen?.host ?? DEFAULT_HOST;
|
|
76
|
+
checkListenerConfig({
|
|
77
|
+
botName: this.#config.botName,
|
|
78
|
+
peers: this.#config.peers,
|
|
79
|
+
credentials: { resolve: () => Promise.resolve(undefined) },
|
|
80
|
+
inflightTtlMs: this.#config.inflightTtlMs ?? DEFAULT_INFLIGHT_TTL_MS,
|
|
81
|
+
maxBodyBytes: this.#config.maxBodyBytes ?? MAX_BODY_BYTES,
|
|
82
|
+
listen: { host },
|
|
83
|
+
});
|
|
84
|
+
const window = this.#config.sentAtWindowMs ?? DEFAULT_SENT_AT_WINDOW_MS;
|
|
85
|
+
wholeMs('sentAtWindowMs', window, 1);
|
|
86
|
+
const inflight = this.#config.inflightTtlMs ?? DEFAULT_INFLIGHT_TTL_MS;
|
|
87
|
+
if (!Number.isInteger(inflight) || inflight <= settings.maxTurnMs) {
|
|
88
|
+
throw new ConfigError(`inflightTtlMs must exceed maxTurnMs: ${inflight} <= ${settings.maxTurnMs}`, ['adapters', 'p2p', 'inflightTtlMs']);
|
|
89
|
+
}
|
|
90
|
+
const bodyLimit = this.#config.maxBodyBytes ?? MAX_BODY_BYTES;
|
|
91
|
+
wholeMs('maxBodyBytes', bodyLimit, 1);
|
|
92
|
+
if (this.#config.waitMs !== undefined)
|
|
93
|
+
wholeMs('waitMs', this.#config.waitMs, 1);
|
|
94
|
+
this.#table = new DedupeTable({
|
|
95
|
+
ttlMs: this.#config.dedupe?.ttlMs ?? DEFAULT_DEDUPE_TTL_MS,
|
|
96
|
+
limit: this.#config.dedupe?.limit ?? DEFAULT_DEDUPE_LIMIT,
|
|
97
|
+
inflightTtlMs: inflight,
|
|
98
|
+
maxTurnMs: settings.maxTurnMs,
|
|
99
|
+
...(this.#options.now === undefined ? {} : { now: this.#options.now }),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
sessionScope(key) {
|
|
103
|
+
return key.startsWith('p2p:') ? 'p2p' : undefined;
|
|
104
|
+
}
|
|
105
|
+
checkSessionKey(key) {
|
|
106
|
+
const shape = tokenProblem('session key', key, SESSION_KEY_MAX);
|
|
107
|
+
if (shape !== undefined)
|
|
108
|
+
return shape;
|
|
109
|
+
if (!key.startsWith('p2p:'))
|
|
110
|
+
return `a p2p session key starts with "p2p:", got "${key}"`;
|
|
111
|
+
const peer = key.slice('p2p:'.length);
|
|
112
|
+
if (peer.length === 0)
|
|
113
|
+
return 'a p2p session key names a peer after "p2p:"';
|
|
114
|
+
return this.#peer(peer) === undefined ? `this adapter addresses no peer "${peer}"` : undefined;
|
|
115
|
+
}
|
|
116
|
+
checkTarget(target) {
|
|
117
|
+
const shape = tokenProblem('target', target, REPLY_TARGET_MAX);
|
|
118
|
+
if (shape !== undefined)
|
|
119
|
+
return shape;
|
|
120
|
+
return this.#peer(target) === undefined ? `this adapter addresses no peer "${target}"` : undefined;
|
|
121
|
+
}
|
|
122
|
+
describeTargets() {
|
|
123
|
+
return this.#config.peers.map((peer) => peer.instance).join(', ');
|
|
124
|
+
}
|
|
125
|
+
status() {
|
|
126
|
+
const snapshot = this.#stats.snapshot();
|
|
127
|
+
const listening = snapshot.state === 'listening';
|
|
128
|
+
const failed = snapshot.state === 'failed';
|
|
129
|
+
return buildStatus({
|
|
130
|
+
channel: this.#config.channel,
|
|
131
|
+
account: this.#config.channel,
|
|
132
|
+
bot: this.#config.botName,
|
|
133
|
+
health: buildListenerHealth({
|
|
134
|
+
listenState: listening ? 'listening' : failed ? 'failed' : 'stopped',
|
|
135
|
+
boundPort: snapshot.boundPort,
|
|
136
|
+
lastAuthenticatedAt: snapshot.lastAuthenticatedAt,
|
|
137
|
+
lastSkewMs: snapshot.lastSkewMs,
|
|
138
|
+
lastFailure: snapshot.lastFailure,
|
|
139
|
+
queueDepth: snapshot.inFlight,
|
|
140
|
+
}),
|
|
141
|
+
counters: countersOf(snapshot),
|
|
142
|
+
capabilities: this.capabilities,
|
|
143
|
+
attributes: { stableMessageId: true },
|
|
144
|
+
quota: { softLimit: 0, hardLimit: 0 },
|
|
145
|
+
sampledAt: snapshot.sampledAt,
|
|
146
|
+
queueDepth: snapshot.inFlight,
|
|
147
|
+
...(this.#table === undefined ? {} : { dedupe: { epoch: this.#table.epoch } }),
|
|
148
|
+
lastSuccessAt: this.#lastSuccessAt,
|
|
149
|
+
lastSendFailure: this.#lastSendFailure === null ? null : { ...this.#lastSendFailure },
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
async send(input) {
|
|
153
|
+
const ports = this.#ports;
|
|
154
|
+
if (ports === undefined) {
|
|
155
|
+
return { ok: false, code: 'unreachable', retryable: true, reason: 'the p2p adapter is not started' };
|
|
156
|
+
}
|
|
157
|
+
const peer = this.#peer(input.target);
|
|
158
|
+
if (peer === undefined) {
|
|
159
|
+
return { ok: false, code: 'no_url', retryable: false, reason: `this adapter addresses no peer "${input.target}"` };
|
|
160
|
+
}
|
|
161
|
+
const to = input.to ?? peer.botName ?? '';
|
|
162
|
+
if (to.length === 0) {
|
|
163
|
+
return {
|
|
164
|
+
ok: false,
|
|
165
|
+
code: 'no_url',
|
|
166
|
+
retryable: false,
|
|
167
|
+
reason: `peer "${peer.instance}" names no bot, so this message has no recipient`,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const target = {
|
|
171
|
+
instance: peer.instance,
|
|
172
|
+
secret: peer.secret,
|
|
173
|
+
...(peer.url === undefined ? {} : { url: peer.url }),
|
|
174
|
+
...(peer.host === undefined ? {} : { host: peer.host }),
|
|
175
|
+
};
|
|
176
|
+
const offered = input.media ?? [];
|
|
177
|
+
const usable = offered.filter((item) => item.url !== null && /^https?:\/\/[^\s]+$/i.test(item.url));
|
|
178
|
+
if (offered.length > 0 && usable.length < offered.length) {
|
|
179
|
+
this.#options.warn?.(`dsbot-channel: dropped ${offered.length - usable.length} media item(s) from ${input.eventId}: p2p carries http(s) urls only`);
|
|
180
|
+
}
|
|
181
|
+
if (usable.length === 0 && input.body.length === 0) {
|
|
182
|
+
return {
|
|
183
|
+
ok: false,
|
|
184
|
+
code: 'no_url',
|
|
185
|
+
retryable: false,
|
|
186
|
+
reason: 'every media item was dropped and the body is empty, so there is nothing a peer could accept',
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
const media = usable.map((item) => item.name === undefined
|
|
190
|
+
? [item.type, item.url]
|
|
191
|
+
: [item.type, item.url, item.name]);
|
|
192
|
+
const result = await sendToPeer({
|
|
193
|
+
peer: target,
|
|
194
|
+
...(input.chainId === undefined ? {} : { chainId: input.chainId }),
|
|
195
|
+
credentials: this.#credentials(ports),
|
|
196
|
+
to,
|
|
197
|
+
body: input.body,
|
|
198
|
+
...(media.length === 0 ? {} : { media }),
|
|
199
|
+
eventId: input.eventId,
|
|
200
|
+
sender: this.#config.botName,
|
|
201
|
+
instance: this.#config.channel,
|
|
202
|
+
...(input.sentAt === undefined ? {} : { sentAt: input.sentAt }),
|
|
203
|
+
...(input.timeoutMs === undefined ? {} : { timeoutMs: input.timeoutMs }),
|
|
204
|
+
...(input.inReplyTo === undefined ? {} : { inReplyTo: input.inReplyTo }),
|
|
205
|
+
...(input.inReplyToText === undefined ? {} : { inReplyToExcerpt: cleanExcerpt(input.inReplyToText) }),
|
|
206
|
+
});
|
|
207
|
+
return this.#settleSend(peer.instance, result);
|
|
208
|
+
}
|
|
209
|
+
async refuse(input) {
|
|
210
|
+
const ports = this.#ports;
|
|
211
|
+
if (ports === undefined) {
|
|
212
|
+
return { ok: false, code: 'unreachable', reason: 'the p2p adapter is not started' };
|
|
213
|
+
}
|
|
214
|
+
const peer = this.#peer(input.target);
|
|
215
|
+
if (peer === undefined) {
|
|
216
|
+
return { ok: false, code: 'no_url', reason: `this adapter addresses no peer "${input.target}"` };
|
|
217
|
+
}
|
|
218
|
+
const to = input.to ?? peer.botName ?? '';
|
|
219
|
+
if (to.length === 0) {
|
|
220
|
+
return { ok: false, code: 'no_url', reason: `peer "${peer.instance}" names no bot, so a refusal has no recipient` };
|
|
221
|
+
}
|
|
222
|
+
const target = {
|
|
223
|
+
instance: peer.instance,
|
|
224
|
+
secret: peer.secret,
|
|
225
|
+
...(peer.url === undefined ? {} : { url: peer.url }),
|
|
226
|
+
...(peer.host === undefined ? {} : { host: peer.host }),
|
|
227
|
+
};
|
|
228
|
+
const result = await sendToPeer({
|
|
229
|
+
peer: target,
|
|
230
|
+
credentials: this.#credentials(ports),
|
|
231
|
+
to,
|
|
232
|
+
body: oneLine(input.note, REFUSE_NOTE_MAX) || DECLINED,
|
|
233
|
+
eventId: `decline:${input.inReplyTo}`,
|
|
234
|
+
chainId: input.chainId,
|
|
235
|
+
sender: this.#config.botName,
|
|
236
|
+
instance: this.#config.channel,
|
|
237
|
+
status: 'declined',
|
|
238
|
+
reason: input.reason,
|
|
239
|
+
final: true,
|
|
240
|
+
});
|
|
241
|
+
this.#settleSend(peer.instance, result);
|
|
242
|
+
return result.ok ? { ok: true } : { ok: false, code: result.code, reason: result.reason };
|
|
243
|
+
}
|
|
244
|
+
async start(ports) {
|
|
245
|
+
const table = this.#table;
|
|
246
|
+
if (table === undefined)
|
|
247
|
+
throw new Error('p2p: checkConfig must run before start');
|
|
248
|
+
this.#ports = ports;
|
|
249
|
+
const listener = await startListener({
|
|
250
|
+
config: {
|
|
251
|
+
botName: this.#config.botName,
|
|
252
|
+
peers: this.#config.peers,
|
|
253
|
+
credentials: this.#credentials(ports),
|
|
254
|
+
inflightTtlMs: this.#config.inflightTtlMs ?? DEFAULT_INFLIGHT_TTL_MS,
|
|
255
|
+
maxBodyBytes: this.#config.maxBodyBytes ?? MAX_BODY_BYTES,
|
|
256
|
+
...(this.#config.sentAtWindowMs === undefined ? {} : { sentAtWindowMs: this.#config.sentAtWindowMs }),
|
|
257
|
+
...(this.#config.waitMs === undefined ? {} : { waitMs: this.#config.waitMs }),
|
|
258
|
+
...(this.#options.warn === undefined ? {} : { warn: this.#options.warn }),
|
|
259
|
+
listen: { host: this.#config.listen?.host ?? DEFAULT_HOST, port: this.#config.listen?.port ?? DEFAULT_PORT },
|
|
260
|
+
},
|
|
261
|
+
table,
|
|
262
|
+
stats: this.#stats,
|
|
263
|
+
onMessage: (request) => {
|
|
264
|
+
void ports
|
|
265
|
+
.onMessage(this.toInbound(request))
|
|
266
|
+
.then((verdict) => {
|
|
267
|
+
if (!verdict.ok)
|
|
268
|
+
this.#options.warn?.(`dsbot-channel: kept ${request.eventId} from ${request.instance} out of a turn: ${verdict.code} ${verdict.reason}`);
|
|
269
|
+
})
|
|
270
|
+
.catch((error) => {
|
|
271
|
+
this.#options.warn?.(`dsbot-channel: handing off ${request.eventId} from ${request.instance} failed: ${String(error)}`);
|
|
272
|
+
});
|
|
273
|
+
},
|
|
274
|
+
onError: (error, request) => {
|
|
275
|
+
this.#options.warn?.(`dsbot-channel: handing off ${request.eventId} from ${request.instance} failed: ${String(error)}`);
|
|
276
|
+
},
|
|
277
|
+
...(this.#config.status === false
|
|
278
|
+
? {}
|
|
279
|
+
: {
|
|
280
|
+
status: {
|
|
281
|
+
route: STATUS_ROUTE,
|
|
282
|
+
report: () => this.status(),
|
|
283
|
+
},
|
|
284
|
+
}),
|
|
285
|
+
});
|
|
286
|
+
return {
|
|
287
|
+
stop: async () => {
|
|
288
|
+
await listener.close();
|
|
289
|
+
},
|
|
290
|
+
address: () => ({ port: listener.address().port }),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
/** Maps a verified wire request onto the inbound message core takes; the adapter owns every field. */
|
|
294
|
+
toInbound(request) {
|
|
295
|
+
const event = toP2PEvent({ ...request, messageId: `p2p-${request.eventId}` });
|
|
296
|
+
return {
|
|
297
|
+
eventKey: event.meta.derived.eventKey,
|
|
298
|
+
sessionKey: `p2p:${request.instance}`,
|
|
299
|
+
replyTarget: request.instance,
|
|
300
|
+
chainId: request.chainId,
|
|
301
|
+
requestId: request.eventId,
|
|
302
|
+
...(request.status === 'declined' ? { suppressed: true } : {}),
|
|
303
|
+
source: event.source,
|
|
304
|
+
author: event.author,
|
|
305
|
+
rawText: request.body,
|
|
306
|
+
body: event.body,
|
|
307
|
+
transport: event.meta.transport,
|
|
308
|
+
declared: event.meta.declared,
|
|
309
|
+
adapter: event.meta.adapter,
|
|
310
|
+
receivedAt: this.#now(),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
#peer(instance) {
|
|
314
|
+
return this.#config.peers.find((peer) => peer.instance === instance);
|
|
315
|
+
}
|
|
316
|
+
#credentials(ports) {
|
|
317
|
+
return {
|
|
318
|
+
resolve: async (reference) => {
|
|
319
|
+
const value = await ports.resolveSecret(reference);
|
|
320
|
+
return value === undefined ? undefined : { value };
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const PEER_GUIDANCE = "This is a peer message from another bot. Answer it with your reply as usual. If it needs no answer, call no_reply instead of staying silent. If you refuse it, call decline with a short reason.";
|
|
2
|
+
export declare const GROUP_GUIDANCE = "This is a message from an external group chat. Your reply is posted to that group. To address one member only, call send_text.";
|
package/dist/guidance.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export const PEER_GUIDANCE = 'This is a peer message from another bot. Answer it with your reply as usual. If it needs no answer, call no_reply instead of staying silent. If you refuse it, call decline with a short reason.';
|
|
2
|
+
export const GROUP_GUIDANCE = 'This is a message from an external group chat. Your reply is posted to that group. To address one member only, call send_text.';
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { AdapterFactory, AdapterMount } from '@dsbot-channel/core/adapter-registry.js';
|
|
2
|
+
import type { P2PAdapterConfig } from './adapter.js';
|
|
3
|
+
/** Reads the transport block a profile declared, refusing a field this adapter never reads. */
|
|
4
|
+
export declare function parseP2PConfig(mount: AdapterMount): P2PAdapterConfig;
|
|
5
|
+
export declare const createP2PAdapter: AdapterFactory;
|
|
6
|
+
export declare const Config: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<NoInfer<{
|
|
7
|
+
channel: import("@deepseek-ai/schemastery").default<string, string, "plain">;
|
|
8
|
+
botName: import("@deepseek-ai/schemastery").default<string, string, "plain">;
|
|
9
|
+
anchor: import("@deepseek-ai/schemastery").default<string, string, "plain">;
|
|
10
|
+
session: import("@deepseek-ai/schemastery").default<string, string, "plain">;
|
|
11
|
+
model: import("@deepseek-ai/schemastery").default<string, string, "plain">;
|
|
12
|
+
reply: import("@deepseek-ai/schemastery").default<any, any, "plain">;
|
|
13
|
+
turn: import("@deepseek-ai/schemastery").default<any, any, "plain">;
|
|
14
|
+
delivery: import("@deepseek-ai/schemastery").default<any, any, "plain">;
|
|
15
|
+
policy: import("@deepseek-ai/schemastery").default<any, any, "plain">;
|
|
16
|
+
pipeline: import("@deepseek-ai/schemastery").default<any, any, "plain">;
|
|
17
|
+
events: import("@deepseek-ai/schemastery").default<any, any, "plain">;
|
|
18
|
+
status: import("@deepseek-ai/schemastery").default<boolean, boolean, "plain">;
|
|
19
|
+
transport: import("@deepseek-ai/schemastery").default<any, any, "plain">;
|
|
20
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
21
|
+
channel: import("@deepseek-ai/schemastery").default<string, string, "plain">;
|
|
22
|
+
botName: import("@deepseek-ai/schemastery").default<string, string, "plain">;
|
|
23
|
+
anchor: import("@deepseek-ai/schemastery").default<string, string, "plain">;
|
|
24
|
+
session: import("@deepseek-ai/schemastery").default<string, string, "plain">;
|
|
25
|
+
model: import("@deepseek-ai/schemastery").default<string, string, "plain">;
|
|
26
|
+
reply: import("@deepseek-ai/schemastery").default<any, any, "plain">;
|
|
27
|
+
turn: import("@deepseek-ai/schemastery").default<any, any, "plain">;
|
|
28
|
+
delivery: import("@deepseek-ai/schemastery").default<any, any, "plain">;
|
|
29
|
+
policy: import("@deepseek-ai/schemastery").default<any, any, "plain">;
|
|
30
|
+
pipeline: import("@deepseek-ai/schemastery").default<any, any, "plain">;
|
|
31
|
+
events: import("@deepseek-ai/schemastery").default<any, any, "plain">;
|
|
32
|
+
status: import("@deepseek-ai/schemastery").default<boolean, boolean, "plain">;
|
|
33
|
+
transport: import("@deepseek-ai/schemastery").default<any, any, "plain">;
|
|
34
|
+
}>>, "plain">;
|
|
35
|
+
export declare const apply: (ctx: import("@deepseek-ai/cordis").Context, config?: Record<string, unknown>) => void;
|
|
36
|
+
export declare const plugin: {
|
|
37
|
+
name: string;
|
|
38
|
+
inject: string[];
|
|
39
|
+
Config: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<NoInfer<{
|
|
40
|
+
channel: import("@deepseek-ai/schemastery").default<string, string, "plain">;
|
|
41
|
+
botName: import("@deepseek-ai/schemastery").default<string, string, "plain">;
|
|
42
|
+
anchor: import("@deepseek-ai/schemastery").default<string, string, "plain">;
|
|
43
|
+
session: import("@deepseek-ai/schemastery").default<string, string, "plain">;
|
|
44
|
+
model: import("@deepseek-ai/schemastery").default<string, string, "plain">;
|
|
45
|
+
reply: import("@deepseek-ai/schemastery").default<any, any, "plain">;
|
|
46
|
+
turn: import("@deepseek-ai/schemastery").default<any, any, "plain">;
|
|
47
|
+
delivery: import("@deepseek-ai/schemastery").default<any, any, "plain">;
|
|
48
|
+
policy: import("@deepseek-ai/schemastery").default<any, any, "plain">;
|
|
49
|
+
pipeline: import("@deepseek-ai/schemastery").default<any, any, "plain">;
|
|
50
|
+
events: import("@deepseek-ai/schemastery").default<any, any, "plain">;
|
|
51
|
+
status: import("@deepseek-ai/schemastery").default<boolean, boolean, "plain">;
|
|
52
|
+
transport: import("@deepseek-ai/schemastery").default<any, any, "plain">;
|
|
53
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
54
|
+
channel: import("@deepseek-ai/schemastery").default<string, string, "plain">;
|
|
55
|
+
botName: import("@deepseek-ai/schemastery").default<string, string, "plain">;
|
|
56
|
+
anchor: import("@deepseek-ai/schemastery").default<string, string, "plain">;
|
|
57
|
+
session: import("@deepseek-ai/schemastery").default<string, string, "plain">;
|
|
58
|
+
model: import("@deepseek-ai/schemastery").default<string, string, "plain">;
|
|
59
|
+
reply: import("@deepseek-ai/schemastery").default<any, any, "plain">;
|
|
60
|
+
turn: import("@deepseek-ai/schemastery").default<any, any, "plain">;
|
|
61
|
+
delivery: import("@deepseek-ai/schemastery").default<any, any, "plain">;
|
|
62
|
+
policy: import("@deepseek-ai/schemastery").default<any, any, "plain">;
|
|
63
|
+
pipeline: import("@deepseek-ai/schemastery").default<any, any, "plain">;
|
|
64
|
+
events: import("@deepseek-ai/schemastery").default<any, any, "plain">;
|
|
65
|
+
status: import("@deepseek-ai/schemastery").default<boolean, boolean, "plain">;
|
|
66
|
+
transport: import("@deepseek-ai/schemastery").default<any, any, "plain">;
|
|
67
|
+
}>>, "plain">;
|
|
68
|
+
apply: (ctx: import("@deepseek-ai/cordis").Context, config?: Record<string, unknown>) => void;
|
|
69
|
+
};
|
|
70
|
+
declare const _default: {
|
|
71
|
+
name: string;
|
|
72
|
+
inject: string[];
|
|
73
|
+
Config: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<NoInfer<{
|
|
74
|
+
channel: import("@deepseek-ai/schemastery").default<string, string, "plain">;
|
|
75
|
+
botName: import("@deepseek-ai/schemastery").default<string, string, "plain">;
|
|
76
|
+
anchor: import("@deepseek-ai/schemastery").default<string, string, "plain">;
|
|
77
|
+
session: import("@deepseek-ai/schemastery").default<string, string, "plain">;
|
|
78
|
+
model: import("@deepseek-ai/schemastery").default<string, string, "plain">;
|
|
79
|
+
reply: import("@deepseek-ai/schemastery").default<any, any, "plain">;
|
|
80
|
+
turn: import("@deepseek-ai/schemastery").default<any, any, "plain">;
|
|
81
|
+
delivery: import("@deepseek-ai/schemastery").default<any, any, "plain">;
|
|
82
|
+
policy: import("@deepseek-ai/schemastery").default<any, any, "plain">;
|
|
83
|
+
pipeline: import("@deepseek-ai/schemastery").default<any, any, "plain">;
|
|
84
|
+
events: import("@deepseek-ai/schemastery").default<any, any, "plain">;
|
|
85
|
+
status: import("@deepseek-ai/schemastery").default<boolean, boolean, "plain">;
|
|
86
|
+
transport: import("@deepseek-ai/schemastery").default<any, any, "plain">;
|
|
87
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
88
|
+
channel: import("@deepseek-ai/schemastery").default<string, string, "plain">;
|
|
89
|
+
botName: import("@deepseek-ai/schemastery").default<string, string, "plain">;
|
|
90
|
+
anchor: import("@deepseek-ai/schemastery").default<string, string, "plain">;
|
|
91
|
+
session: import("@deepseek-ai/schemastery").default<string, string, "plain">;
|
|
92
|
+
model: import("@deepseek-ai/schemastery").default<string, string, "plain">;
|
|
93
|
+
reply: import("@deepseek-ai/schemastery").default<any, any, "plain">;
|
|
94
|
+
turn: import("@deepseek-ai/schemastery").default<any, any, "plain">;
|
|
95
|
+
delivery: import("@deepseek-ai/schemastery").default<any, any, "plain">;
|
|
96
|
+
policy: import("@deepseek-ai/schemastery").default<any, any, "plain">;
|
|
97
|
+
pipeline: import("@deepseek-ai/schemastery").default<any, any, "plain">;
|
|
98
|
+
events: import("@deepseek-ai/schemastery").default<any, any, "plain">;
|
|
99
|
+
status: import("@deepseek-ai/schemastery").default<boolean, boolean, "plain">;
|
|
100
|
+
transport: import("@deepseek-ai/schemastery").default<any, any, "plain">;
|
|
101
|
+
}>>, "plain">;
|
|
102
|
+
apply: (ctx: import("@deepseek-ai/cordis").Context, config?: Record<string, unknown>) => void;
|
|
103
|
+
};
|
|
104
|
+
export default _default;
|