dsh-telegram-connector 0.15.1 → 0.15.2
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/CHANGELOG.md +18 -0
- package/COMPATIBILITY.md +2 -1
- package/README.md +4 -3
- package/docs/DESIGN.md +3 -1
- package/docs/STANDALONE.md +9 -4
- package/lib/client.js +129 -61
- package/lib/index.d.ts +24 -43
- package/lib/index.js +55 -17
- package/lib/index.js.map +1 -1
- package/lib/telegram.d.ts +7 -1
- package/lib/telegram.js +8 -0
- package/lib/telegram.js.map +1 -1
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.15.2
|
|
4
|
+
|
|
5
|
+
- **The Plugins-page configuration page now actually appears and saves.** The
|
|
6
|
+
three editable fields (`botToken`, `allowedUserIds`, `adminUserId`) are
|
|
7
|
+
`.volatile()`, so DSH projects the row's settings namespace and passes the
|
|
8
|
+
`form` object to `plugins.row.config`; without that the Configure page opened
|
|
9
|
+
with no form. The client bundle now declares the real
|
|
10
|
+
`@deepseek-ai/dsh-client-ui-plugin-manager` inject, runs every React hook
|
|
11
|
+
before the loading/unavailable returns, and uses the current
|
|
12
|
+
`--dsw-alias-*` theme tokens.
|
|
13
|
+
- **Live parameters.** A saved bot token updates the running `TelegramClient`
|
|
14
|
+
and starts polling if the connector was idle; allowed ids and the admin id
|
|
15
|
+
are read from the volatile config on every check. The connector now loads
|
|
16
|
+
with an empty token so the Configure page is reachable, and warns instead of
|
|
17
|
+
returning early.
|
|
18
|
+
- Tests cover the volatile config shape, the live token path and a rendered
|
|
19
|
+
ready snapshot.
|
|
20
|
+
|
|
3
21
|
## 0.15.1
|
|
4
22
|
|
|
5
23
|
- Release-check fix: the `0.15.0` tag was cut without its COMPATIBILITY row and
|
package/COMPATIBILITY.md
CHANGED
|
@@ -6,7 +6,8 @@ every release.
|
|
|
6
6
|
|
|
7
7
|
| Connector | DSH | Status | Notes |
|
|
8
8
|
|---|---|---|---|
|
|
9
|
-
| 0.15.
|
|
9
|
+
| 0.15.2 | `0.2.1-rc.5` | needs UAT | config page fixed: volatile fields expose the form, correct client inject, hooks run before loading returns, DSH theme tokens; saving a token starts polling without a reload |
|
|
10
|
+
| 0.15.1 | `0.2.1-rc.5` | previous | config page attempt; the form stayed hidden because the fields were not volatile and the client inject named a package that does not exist |
|
|
10
11
|
| 0.14.1 | `0.2.1-rc.5` | needs UAT | component actions no longer restart the runtime; the tools load on the next start |
|
|
11
12
|
| 0.14.0 | `0.2.1-rc.5` | needs UAT | one command surface: `/menu` card, `/plugins` subcommands, `/components`; aliases and `/man` removed |
|
|
12
13
|
| 0.13.1 | `0.2.1-rc.5` | needs UAT | `/system status` probes the advertised web address, so a LAN-only bind no longer reports loopback as unreachable |
|
package/README.md
CHANGED
|
@@ -154,7 +154,7 @@ install without the setup.
|
|
|
154
154
|
The connector is published to npm as an exact version:
|
|
155
155
|
|
|
156
156
|
```sh
|
|
157
|
-
dsh plugin --profile web add dsh-telegram-connector@0.15.
|
|
157
|
+
dsh plugin --profile web add dsh-telegram-connector@0.15.2
|
|
158
158
|
```
|
|
159
159
|
|
|
160
160
|
The registry tarball already contains the built `lib/`, so there is no
|
|
@@ -168,7 +168,8 @@ On DSH `0.2.1-rc.5` or newer, open **Plugins** → **dsh-telegram-connector** an
|
|
|
168
168
|
press **Configure** on the `dsh-telegram-connector` row. The page has three
|
|
169
169
|
fields — **bot token**, **allowed user ids** and **admin user id** — and
|
|
170
170
|
**Save** writes the row's config into the profile's user layer. No `.env` edit
|
|
171
|
-
or restart is needed; the connector
|
|
171
|
+
or restart is needed; the connector reconnects with a saved token and starts
|
|
172
|
+
polling, and allowed/admin ids are read on each check.
|
|
172
173
|
|
|
173
174
|
That is the standalone path: the setup is not required, and the connector does
|
|
174
175
|
not assume the setup's supervisor or updater. Commands that need them
|
|
@@ -184,7 +185,7 @@ cat >> "$DSH_HOME/profiles/web/pnpm-workspace.yaml" <<'YAML'
|
|
|
184
185
|
allowBuilds:
|
|
185
186
|
dsh-telegram-connector@https://github.com/vladtsvirko/dsh-telegram-connector.git: true
|
|
186
187
|
YAML
|
|
187
|
-
dsh plugin --profile web add github:vladtsvirko/dsh-telegram-connector#v0.15.
|
|
188
|
+
dsh plugin --profile web add github:vladtsvirko/dsh-telegram-connector#v0.15.2
|
|
188
189
|
```
|
|
189
190
|
|
|
190
191
|
That path runs the package's `prepare` build; pnpm ≥ 10 refuses it until the
|
package/docs/DESIGN.md
CHANGED
|
@@ -316,7 +316,9 @@ and directories.
|
|
|
316
316
|
Commit/Roll back, component Enable/Disable/Port, the update card's **Cancel** and Take
|
|
317
317
|
over all check `adminUserId`. The allowlist only decides who may talk to the
|
|
318
318
|
agent; a button that changes the deployment is an admin action.
|
|
319
|
-
- The bot token comes from the environment
|
|
319
|
+
- The bot token comes from the profile's Plugins config (the environment is the
|
|
320
|
+
fallback), never from `cordis.patch.yml`. The same applies to the allowlist
|
|
321
|
+
and admin id.
|
|
320
322
|
- The bot is a group admin and can delete topics: `/dangerous cleanup` requires
|
|
321
323
|
an explicit `confirm`, and `/forget` never deletes anything.
|
|
322
324
|
- Plugin mutations are the highest-risk surface: a bundle's code and install
|
package/docs/STANDALONE.md
CHANGED
|
@@ -17,7 +17,7 @@ The same package runs in any DSH profile.
|
|
|
17
17
|
|
|
18
18
|
```sh
|
|
19
19
|
# Published package: the tarball already contains lib/, so no build script.
|
|
20
|
-
dsh plugin --profile web add dsh-telegram-connector@0.
|
|
20
|
+
dsh plugin --profile web add dsh-telegram-connector@0.15.2
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
From the source repository instead:
|
|
@@ -27,11 +27,16 @@ cat >> "$DSH_HOME/profiles/web/pnpm-workspace.yaml" <<'YAML'
|
|
|
27
27
|
allowBuilds:
|
|
28
28
|
dsh-telegram-connector@https://github.com/vladtsvirko/dsh-telegram-connector.git: true
|
|
29
29
|
YAML
|
|
30
|
-
dsh plugin --profile web add github:vladtsvirko/dsh-telegram-connector#v0.
|
|
30
|
+
dsh plugin --profile web add github:vladtsvirko/dsh-telegram-connector#v0.15.2
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
Open **Plugins** → **dsh-telegram-connector** → **Configure** and fill in the
|
|
34
|
+
bot token, allowed user ids and optional admin id. **Save** stores them in the
|
|
35
|
+
profile's user layer; a saved token starts polling without a runtime restart,
|
|
36
|
+
and the ids are read on every check. No environment file is required.
|
|
37
|
+
|
|
38
|
+
The bundle patch still reads deployment values from the environment as the
|
|
39
|
+
fallback:
|
|
35
40
|
|
|
36
41
|
```sh
|
|
37
42
|
export DSH_TELEGRAM_BOT_TOKEN='123456:ABC…'
|
package/lib/client.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Browser half: the configuration page for the `dsh-telegram-connector` row.
|
|
3
3
|
*
|
|
4
4
|
* DSH's Plugins page declares the `plugins.row.config` slot; this module
|
|
5
|
-
* registers the row's page with the
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* registers the row's page with the values a fresh standalone install needs.
|
|
6
|
+
* The host half reads the same volatile keys from its Config schema; the patch
|
|
7
|
+
* keeps the environment as the fallback, and a value saved here is the
|
|
8
8
|
* profile's user layer, which wins over it.
|
|
9
9
|
*/
|
|
10
10
|
window.__ModuleLoader__.load({
|
|
@@ -18,67 +18,99 @@ window.__ModuleLoader__.load({
|
|
|
18
18
|
const h = React.createElement
|
|
19
19
|
|
|
20
20
|
const KEY = 'dsh-telegram-connector#dsh-telegram-connector'
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
21
|
+
const styles = `
|
|
22
|
+
.dsh-telegram-form { display: flex; flex-direction: column; gap: 16px; max-width: 560px; color: var(--dsw-alias-label-primary); font-size: 13px; }
|
|
23
|
+
.dsh-telegram-form .panel { display: flex; flex-direction: column; gap: 4px; padding: 10px 12px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; background: var(--dsw-alias-bg-layer-2); }
|
|
24
|
+
.dsh-telegram-form .row { display: flex; align-items: center; gap: 6px; font-weight: 500; }
|
|
25
|
+
.dsh-telegram-form .dot { flex: none; width: 6px; height: 6px; border-radius: 50%; background: var(--dsw-alias-state-idle-primary); }
|
|
26
|
+
.dsh-telegram-form .dot.ok { background: var(--dsw-alias-state-success-primary); }
|
|
27
|
+
.dsh-telegram-form .muted { color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 1.45; }
|
|
28
|
+
.dsh-telegram-form .field { display: flex; flex-direction: column; gap: 6px; }
|
|
29
|
+
.dsh-telegram-form .label { font-weight: 500; }
|
|
30
|
+
.dsh-telegram-form .hint { color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 1.4; }
|
|
31
|
+
.dsh-telegram-form .input { box-sizing: border-box; width: 100%; min-height: 32px; padding: 6px 10px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); font: inherit; font-size: 13px; }
|
|
32
|
+
.dsh-telegram-form .input:focus { outline: 1px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }
|
|
33
|
+
.dsh-telegram-form .input:disabled { opacity: 0.6; }
|
|
34
|
+
.dsh-telegram-form .actions { display: flex; align-items: center; gap: 10px; }
|
|
35
|
+
.dsh-telegram-form .primary { min-height: 32px; padding: 0 14px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 6px; background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); font: inherit; font-size: 13px; font-weight: 500; cursor: pointer; }
|
|
36
|
+
.dsh-telegram-form .primary:hover:not(:disabled) { background: var(--dsw-alias-bg-layer-2); }
|
|
37
|
+
.dsh-telegram-form .primary:disabled { opacity: 0.55; cursor: default; }
|
|
38
|
+
.dsh-telegram-form .message { color: var(--dsw-alias-label-secondary); font-size: 12px; }
|
|
39
|
+
`
|
|
31
40
|
|
|
32
|
-
function
|
|
41
|
+
function Field({ label, hint, children }) {
|
|
33
42
|
return h(
|
|
34
43
|
'label',
|
|
35
|
-
{
|
|
36
|
-
h('span', {
|
|
37
|
-
hint === undefined ? null : h('span', {
|
|
38
|
-
|
|
44
|
+
{ className: 'field' },
|
|
45
|
+
h('span', { className: 'label' }, label),
|
|
46
|
+
hint === undefined ? null : h('span', { className: 'hint' }, hint),
|
|
47
|
+
children,
|
|
39
48
|
)
|
|
40
49
|
}
|
|
41
50
|
|
|
42
51
|
function parseIds(text) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
.
|
|
48
|
-
.
|
|
52
|
+
const parts = text.split(/[\s,]+/).map((part) => part.trim()).filter((part) => part.length > 0)
|
|
53
|
+
const ids = []
|
|
54
|
+
for (const part of parts) {
|
|
55
|
+
const id = Number(part)
|
|
56
|
+
if (!Number.isInteger(id) || id <= 0) return undefined
|
|
57
|
+
ids.push(id)
|
|
58
|
+
}
|
|
59
|
+
return ids
|
|
49
60
|
}
|
|
50
61
|
|
|
51
62
|
function TelegramConfigForm({ form }) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
if (state.status === 'unavailable') {
|
|
57
|
-
return h('p', null, 'This plugin is not loaded, so it cannot be configured right now.')
|
|
58
|
-
}
|
|
59
|
-
if (state.status === 'loading') {
|
|
60
|
-
return h('p', null, 'Loading the current values…')
|
|
61
|
-
}
|
|
62
|
-
const value = state.value ?? {}
|
|
63
|
+
// Every hook runs before any early return: the page transitions through
|
|
64
|
+
// loading/unavailable snapshots and a conditional hook would crash React.
|
|
65
|
+
const state = form === undefined ? undefined : form.state
|
|
66
|
+
const value = (state === undefined ? undefined : state.value) ?? {}
|
|
63
67
|
const [botToken, setBotToken] = React.useState(String(value.botToken ?? ''))
|
|
64
|
-
const [allowed, setAllowed] = React.useState(
|
|
65
|
-
|
|
68
|
+
const [allowed, setAllowed] = React.useState(
|
|
69
|
+
Array.isArray(value.allowedUserIds) ? value.allowedUserIds.join(', ') : '',
|
|
70
|
+
)
|
|
71
|
+
const [admin, setAdmin] = React.useState(
|
|
72
|
+
value.adminUserId === undefined ? '' : String(value.adminUserId),
|
|
73
|
+
)
|
|
66
74
|
const [saving, setSaving] = React.useState(false)
|
|
67
75
|
const [message, setMessage] = React.useState('')
|
|
68
76
|
|
|
77
|
+
const revision = state === undefined ? undefined : state.revision
|
|
69
78
|
React.useEffect(() => {
|
|
70
|
-
const next = state.value ?? {}
|
|
79
|
+
const next = state === undefined ? {} : (state.value ?? {})
|
|
71
80
|
setBotToken(String(next.botToken ?? ''))
|
|
72
81
|
setAllowed(Array.isArray(next.allowedUserIds) ? next.allowedUserIds.join(', ') : '')
|
|
73
82
|
setAdmin(next.adminUserId === undefined ? '' : String(next.adminUserId))
|
|
74
|
-
}, [
|
|
83
|
+
}, [revision])
|
|
84
|
+
|
|
85
|
+
if (state === undefined) {
|
|
86
|
+
return h('p', null, 'Loading the configuration…')
|
|
87
|
+
}
|
|
88
|
+
if (state.status === 'unavailable') {
|
|
89
|
+
return h('p', null, 'This plugin is not loaded, so it cannot be configured right now.')
|
|
90
|
+
}
|
|
91
|
+
if (state.status === 'loading') {
|
|
92
|
+
return h('p', null, 'Loading the current values…')
|
|
93
|
+
}
|
|
75
94
|
|
|
95
|
+
const savedToken = String(value.botToken ?? '')
|
|
96
|
+
const savedAllowed = Array.isArray(value.allowedUserIds) ? value.allowedUserIds : []
|
|
97
|
+
const savedAdmin = value.adminUserId
|
|
98
|
+
const tokenSet = savedToken.trim().length > 0
|
|
99
|
+
const configured = tokenSet && savedAllowed.length > 0
|
|
76
100
|
const disabled = !state.writable || saving
|
|
101
|
+
const saved = state.user ?? {}
|
|
102
|
+
const source = (key) => (Object.prototype.hasOwnProperty.call(saved, key) ? 'saved here' : 'environment fallback')
|
|
103
|
+
|
|
77
104
|
const save = async () => {
|
|
78
105
|
const ids = parseIds(allowed)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
106
|
+
if (ids === undefined) {
|
|
107
|
+
setMessage('Allowed user ids must be positive integers, separated by commas or spaces.')
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
const adminText = admin.trim()
|
|
111
|
+
const adminId = adminText.length === 0 ? undefined : Number(adminText)
|
|
112
|
+
if (adminId !== undefined && (!Number.isInteger(adminId) || adminId <= 0)) {
|
|
113
|
+
setMessage('The admin user id must be a positive integer, or empty to use the first allowed id.')
|
|
82
114
|
return
|
|
83
115
|
}
|
|
84
116
|
setSaving(true)
|
|
@@ -92,7 +124,13 @@ window.__ModuleLoader__.load({
|
|
|
92
124
|
: { op: 'set', path: ['adminUserId'], value: adminId },
|
|
93
125
|
]
|
|
94
126
|
const ok = await form.mutate(ops, state.revision)
|
|
95
|
-
|
|
127
|
+
if (!ok) {
|
|
128
|
+
setMessage('The runtime did not accept these values; they were left for you to correct.')
|
|
129
|
+
} else if (botToken.trim().length === 0 || ids.length === 0) {
|
|
130
|
+
setMessage('Saved. Set a bot token and at least one allowed user id to start the connector.')
|
|
131
|
+
} else {
|
|
132
|
+
setMessage('Saved. The connector uses these values immediately.')
|
|
133
|
+
}
|
|
96
134
|
} catch (error) {
|
|
97
135
|
setMessage(`Save failed: ${String(error)}`)
|
|
98
136
|
} finally {
|
|
@@ -102,58 +140,88 @@ window.__ModuleLoader__.load({
|
|
|
102
140
|
|
|
103
141
|
return h(
|
|
104
142
|
'div',
|
|
105
|
-
|
|
106
|
-
h('
|
|
107
|
-
|
|
108
|
-
'
|
|
109
|
-
|
|
143
|
+
{ className: 'dsh-telegram-form' },
|
|
144
|
+
h('style', null, styles),
|
|
145
|
+
h(
|
|
146
|
+
'div',
|
|
147
|
+
{ className: 'panel' },
|
|
148
|
+
h(
|
|
149
|
+
'div',
|
|
150
|
+
{ className: 'row' },
|
|
151
|
+
h('span', { className: configured ? 'dot ok' : 'dot' }),
|
|
152
|
+
h('span', null, configured ? 'Configured' : 'Not configured yet'),
|
|
153
|
+
),
|
|
154
|
+
h(
|
|
155
|
+
'div',
|
|
156
|
+
{ className: 'muted' },
|
|
157
|
+
`Bot token ${tokenSet ? 'set' : 'not set'} · ${savedAllowed.length} allowed user(s) · admin ${savedAdmin === undefined ? 'first allowed' : String(savedAdmin)}`,
|
|
158
|
+
),
|
|
159
|
+
h(
|
|
160
|
+
'div',
|
|
161
|
+
{ className: 'muted' },
|
|
162
|
+
`Bot token: ${source('botToken')} · allowed ids: ${source('allowedUserIds')} · admin: ${source('adminUserId')}`,
|
|
163
|
+
),
|
|
164
|
+
h('div', { className: 'muted' }, 'Saving writes this profile and applies without restarting the runtime.'),
|
|
165
|
+
),
|
|
166
|
+
h(
|
|
167
|
+
Field,
|
|
168
|
+
{
|
|
169
|
+
label: 'Bot token',
|
|
170
|
+
hint: 'The token from @BotFather. It is stored in this profile; avoid screenshots and commits.',
|
|
171
|
+
},
|
|
110
172
|
h('input', {
|
|
111
173
|
type: 'password',
|
|
174
|
+
className: 'input',
|
|
112
175
|
value: botToken,
|
|
113
176
|
disabled,
|
|
114
|
-
placeholder: '
|
|
115
|
-
style: inputStyle,
|
|
177
|
+
placeholder: '123456789:AA…',
|
|
116
178
|
onChange: (event) => setBotToken(event.target.value),
|
|
117
179
|
}),
|
|
118
180
|
),
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
181
|
+
h(
|
|
182
|
+
Field,
|
|
183
|
+
{
|
|
184
|
+
label: 'Allowed user ids',
|
|
185
|
+
hint: 'Numeric Telegram ids, comma or space separated. Everyone here can talk to the agent and use its sessions.',
|
|
186
|
+
},
|
|
122
187
|
h('input', {
|
|
123
188
|
type: 'text',
|
|
189
|
+
className: 'input',
|
|
124
190
|
value: allowed,
|
|
125
191
|
disabled,
|
|
126
192
|
placeholder: '142283532, 777000111',
|
|
127
|
-
style: inputStyle,
|
|
128
193
|
onChange: (event) => setAllowed(event.target.value),
|
|
129
194
|
}),
|
|
130
195
|
),
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
196
|
+
h(
|
|
197
|
+
Field,
|
|
198
|
+
{
|
|
199
|
+
label: 'Admin user id',
|
|
200
|
+
hint: 'Optional. One allowed id that may update, restart, roll back and manage plugins; empty uses the first allowed id.',
|
|
201
|
+
},
|
|
134
202
|
h('input', {
|
|
135
203
|
type: 'text',
|
|
204
|
+
className: 'input',
|
|
136
205
|
value: admin,
|
|
137
206
|
disabled,
|
|
138
207
|
placeholder: 'optional',
|
|
139
|
-
style: inputStyle,
|
|
140
208
|
onChange: (event) => setAdmin(event.target.value),
|
|
141
209
|
}),
|
|
142
210
|
),
|
|
143
211
|
h(
|
|
144
212
|
'div',
|
|
145
|
-
{
|
|
213
|
+
{ className: 'actions' },
|
|
146
214
|
h(
|
|
147
215
|
'button',
|
|
148
216
|
{
|
|
149
217
|
type: 'button',
|
|
218
|
+
className: 'primary',
|
|
150
219
|
disabled,
|
|
151
220
|
onClick: save,
|
|
152
|
-
style: { padding: '6px 14px', borderRadius: 6, cursor: disabled ? 'default' : 'pointer' },
|
|
153
221
|
},
|
|
154
222
|
saving ? 'Saving…' : 'Save',
|
|
155
223
|
),
|
|
156
|
-
message.length === 0 ? null : h('span', {
|
|
224
|
+
message.length === 0 ? null : h('span', { className: 'message' }, message),
|
|
157
225
|
),
|
|
158
226
|
)
|
|
159
227
|
}
|
package/lib/index.d.ts
CHANGED
|
@@ -10,54 +10,35 @@
|
|
|
10
10
|
*
|
|
11
11
|
* @module dsh-telegram-connector
|
|
12
12
|
*/
|
|
13
|
-
import type { Context } from '@deepseek-ai/cordis';
|
|
13
|
+
import type { Context, Volatile } from '@deepseek-ai/cordis';
|
|
14
14
|
import Schema from '@deepseek-ai/schemastery';
|
|
15
15
|
export declare const name = "dsh-telegram-connector";
|
|
16
16
|
export declare const inject: string[];
|
|
17
|
-
export
|
|
18
|
-
botToken
|
|
19
|
-
allowedUserIds
|
|
20
|
-
adminUserId
|
|
21
|
-
workspaceRoot
|
|
22
|
-
provider
|
|
23
|
-
model
|
|
24
|
-
editIntervalMs
|
|
25
|
-
pollTimeoutSeconds
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
botToken: Schema<string, string, "defined">;
|
|
38
|
-
allowedUserIds: Schema<number[], number[], "defined">;
|
|
39
|
-
adminUserId: Schema<number, number, "plain">;
|
|
40
|
-
workspaceRoot: Schema<string, string, "defined">;
|
|
41
|
-
provider: Schema<string, string, "plain">;
|
|
42
|
-
model: Schema<string, string, "plain">;
|
|
43
|
-
editIntervalMs: Schema<number, number, "defined">;
|
|
44
|
-
pollTimeoutSeconds: Schema<number, number, "defined">;
|
|
45
|
-
/** Deadline for one Bot API call; a hung call must not freeze a topic. */
|
|
46
|
-
requestTimeoutSeconds: Schema<number, number, "defined">;
|
|
47
|
-
downloadTimeoutSeconds: Schema<number, number, "defined">;
|
|
48
|
-
reconcileIntervalSeconds: Schema<number, number, "defined">;
|
|
49
|
-
processMode: Schema<"off" | "summary" | "live", "off" | "summary" | "live", "defined">;
|
|
50
|
-
agentPreset: Schema<string, string, "plain">;
|
|
51
|
-
reactionsEnabled: Schema<boolean, boolean, "defined">;
|
|
52
|
-
readReaction: Schema<string, string, "defined">;
|
|
53
|
-
streamingReaction: Schema<string, string, "defined">;
|
|
54
|
-
stagedReaction: Schema<string, string, "defined">;
|
|
55
|
-
}>>, "plain">;
|
|
17
|
+
export interface ConnectorConfigInput {
|
|
18
|
+
botToken?: string;
|
|
19
|
+
allowedUserIds?: number[];
|
|
20
|
+
adminUserId?: number;
|
|
21
|
+
workspaceRoot?: string;
|
|
22
|
+
provider?: string;
|
|
23
|
+
model?: string;
|
|
24
|
+
editIntervalMs?: number;
|
|
25
|
+
pollTimeoutSeconds?: number;
|
|
26
|
+
requestTimeoutSeconds?: number;
|
|
27
|
+
downloadTimeoutSeconds?: number;
|
|
28
|
+
reconcileIntervalSeconds?: number;
|
|
29
|
+
processMode?: 'off' | 'summary' | 'live';
|
|
30
|
+
agentPreset?: string;
|
|
31
|
+
reactionsEnabled?: boolean;
|
|
32
|
+
readReaction?: string;
|
|
33
|
+
streamingReaction?: string;
|
|
34
|
+
stagedReaction?: string;
|
|
35
|
+
}
|
|
36
|
+
export declare const Config: Schema<ConnectorConfigInput, ConnectorConfig>;
|
|
56
37
|
export interface ConnectorConfig {
|
|
57
|
-
botToken: string
|
|
58
|
-
allowedUserIds: number[]
|
|
38
|
+
botToken: Volatile<string>;
|
|
39
|
+
allowedUserIds: Volatile<number[]>;
|
|
59
40
|
/** The one account that may run update/restart/rollback/plugins commands. */
|
|
60
|
-
adminUserId
|
|
41
|
+
adminUserId: Volatile<number | undefined>;
|
|
61
42
|
workspaceRoot?: string;
|
|
62
43
|
provider?: string;
|
|
63
44
|
model?: string;
|
package/lib/index.js
CHANGED
|
@@ -41,9 +41,9 @@ import { bindingsPath, findWorkspace, loadBindings, removeSession, saveBindings,
|
|
|
41
41
|
export const name = 'dsh-telegram-connector';
|
|
42
42
|
export const inject = ['agents', 'sessions'];
|
|
43
43
|
export const Config = Schema.object({
|
|
44
|
-
botToken: Schema.string().default(''),
|
|
45
|
-
allowedUserIds: Schema.array(Schema.number()).default([]),
|
|
46
|
-
adminUserId: Schema.number(),
|
|
44
|
+
botToken: Schema.string().default('').volatile(),
|
|
45
|
+
allowedUserIds: Schema.array(Schema.number()).default([]).volatile(),
|
|
46
|
+
adminUserId: Schema.number().volatile(),
|
|
47
47
|
workspaceRoot: Schema.string().default(''),
|
|
48
48
|
provider: Schema.string(),
|
|
49
49
|
model: Schema.string(),
|
|
@@ -101,9 +101,13 @@ function truncate(text, limit) {
|
|
|
101
101
|
return clean.length <= limit ? clean : `${clean.slice(0, limit - 1)}…`;
|
|
102
102
|
}
|
|
103
103
|
export function apply(ctx, config) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
// The three editable fields are volatile: the Plugins page writes them and
|
|
105
|
+
// this row reads them through get(). Everything else stays ordinary config.
|
|
106
|
+
const botToken = () => config.botToken.get();
|
|
107
|
+
const allowedUserIds = () => config.allowedUserIds.get();
|
|
108
|
+
const adminUserId = () => config.adminUserId.get();
|
|
109
|
+
if (botToken().trim().length === 0) {
|
|
110
|
+
ctx.logger.warn('dsh-telegram-connector: botToken is empty; configure it on the Plugins page');
|
|
107
111
|
}
|
|
108
112
|
const agentPresetService = () => {
|
|
109
113
|
const get = ctx.get;
|
|
@@ -112,14 +116,14 @@ export function apply(ctx, config) {
|
|
|
112
116
|
const requestTimeoutMs = Math.max(5, config.requestTimeoutSeconds ?? 60) * 1000;
|
|
113
117
|
const pollTimeoutMs = Math.max(5, config.pollTimeoutSeconds ?? 25) * 1000 + 30_000;
|
|
114
118
|
const downloadTimeoutMs = Math.max(10, config.downloadTimeoutSeconds ?? 120) * 1000;
|
|
115
|
-
const client = new TelegramClient(
|
|
119
|
+
const client = new TelegramClient(botToken(), { requestTimeoutMs, pollTimeoutMs, downloadTimeoutMs });
|
|
116
120
|
const agents = ctx.agents;
|
|
117
121
|
// Publisher bots are send-only; the main bot stays the only poller. Tokens come
|
|
118
122
|
// from the environment so they never land in the profile patch.
|
|
119
123
|
const publisherTokens = (process.env.DSH_TELEGRAM_PUBLISHER_TOKENS ?? '')
|
|
120
124
|
.split(/[,\s]+/)
|
|
121
125
|
.map((token) => token.trim())
|
|
122
|
-
.filter((token) => token.length > 0 && token !==
|
|
126
|
+
.filter((token) => token.length > 0 && token !== botToken());
|
|
123
127
|
const botHandles = [
|
|
124
128
|
{ label: 'main', client, main: true },
|
|
125
129
|
...publisherTokens.map((token, index) => ({
|
|
@@ -155,6 +159,10 @@ export function apply(ctx, config) {
|
|
|
155
159
|
let bindings = { version: 2, workspaces: {}, sessions: {} };
|
|
156
160
|
let offset = 0;
|
|
157
161
|
let stopped = false;
|
|
162
|
+
/** True after start() finished its one-time setup. */
|
|
163
|
+
let started = false;
|
|
164
|
+
/** True while pollLoop owns getUpdates; a token saved later starts it once. */
|
|
165
|
+
let polling = false;
|
|
158
166
|
let lastErrorAt = 0;
|
|
159
167
|
let reconcileTimer;
|
|
160
168
|
let reconciling = false;
|
|
@@ -169,12 +177,12 @@ export function apply(ctx, config) {
|
|
|
169
177
|
const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
170
178
|
const updaterToken = process.env.DSH_UPDATER_TOKEN ?? '';
|
|
171
179
|
const updaterUrl = (process.env.DSH_UPDATER_URL ?? 'http://updater:8787').replace(/\/+$/, '');
|
|
172
|
-
if (
|
|
180
|
+
if (allowedUserIds().length === 0) {
|
|
173
181
|
logger.warn('dsh-telegram-connector: allowedUserIds is empty; every message will be refused');
|
|
174
182
|
}
|
|
175
183
|
/** Last error an agent reported, shown by `/system status`. */
|
|
176
184
|
let lastAgentError;
|
|
177
|
-
const isAllowed = (userId) => userId !== undefined &&
|
|
185
|
+
const isAllowed = (userId) => userId !== undefined && allowedUserIds().includes(userId);
|
|
178
186
|
/**
|
|
179
187
|
* Who may run profile-global commands (update, restart, rollback, plugins).
|
|
180
188
|
*
|
|
@@ -185,8 +193,8 @@ export function apply(ctx, config) {
|
|
|
185
193
|
const isAdmin = (userId) => {
|
|
186
194
|
if (userId === undefined)
|
|
187
195
|
return false;
|
|
188
|
-
if (
|
|
189
|
-
return userId ===
|
|
196
|
+
if (adminUserId() !== undefined)
|
|
197
|
+
return userId === adminUserId();
|
|
190
198
|
// No admin configured (an upgraded deployment, or one set up by hand): the
|
|
191
199
|
// owner of the control chat is the admin, so an upgrade cannot lock anyone
|
|
192
200
|
// out. In a private chat the chat id *is* the user id.
|
|
@@ -202,9 +210,9 @@ export function apply(ctx, config) {
|
|
|
202
210
|
const sender = message.from?.id;
|
|
203
211
|
if (isAdmin(sender))
|
|
204
212
|
return true;
|
|
205
|
-
const who =
|
|
213
|
+
const who = adminUserId() === undefined
|
|
206
214
|
? 'the owner of this chat (`DSH_ADMIN_USER_ID` is not set)'
|
|
207
|
-
: `admin ${String(
|
|
215
|
+
: `admin ${String(adminUserId())}`;
|
|
208
216
|
void reply(message, `only ${who} can do that — it changes the deployment for everyone.`);
|
|
209
217
|
return false;
|
|
210
218
|
};
|
|
@@ -1604,11 +1612,11 @@ export function apply(ctx, config) {
|
|
|
1604
1612
|
].join('\n'));
|
|
1605
1613
|
};
|
|
1606
1614
|
/** Who may run profile-global commands, and how that was decided. */
|
|
1607
|
-
const adminLine = () =>
|
|
1615
|
+
const adminLine = () => adminUserId() === undefined
|
|
1608
1616
|
? bindings.controlChatId === undefined
|
|
1609
1617
|
? 'admin: any allowed user until the bot chat is used (set `DSH_ADMIN_USER_ID` to name one)'
|
|
1610
1618
|
: 'admin: the owner of this chat (set `DSH_ADMIN_USER_ID` to name one explicitly)'
|
|
1611
|
-
: `admin: ${String(
|
|
1619
|
+
: `admin: ${String(adminUserId())}`;
|
|
1612
1620
|
/** The provider key this deployment is supposed to have. */
|
|
1613
1621
|
const providerKeyNames = () => {
|
|
1614
1622
|
const declared = (process.env.DSH_PROVIDER_API_KEY_ENV ?? '').trim();
|
|
@@ -5284,6 +5292,14 @@ export function apply(ctx, config) {
|
|
|
5284
5292
|
stderr(`${bot.label}: member of ${stats.present}/${total} bound group(s)`);
|
|
5285
5293
|
}
|
|
5286
5294
|
};
|
|
5295
|
+
const ensurePolling = () => {
|
|
5296
|
+
if (!started || polling || stopped || botToken().trim().length === 0)
|
|
5297
|
+
return;
|
|
5298
|
+
polling = true;
|
|
5299
|
+
void pollLoop().finally(() => {
|
|
5300
|
+
polling = false;
|
|
5301
|
+
});
|
|
5302
|
+
};
|
|
5287
5303
|
const start = async () => {
|
|
5288
5304
|
// A file that could not be read is set aside by the loader; say so in the
|
|
5289
5305
|
// chat, once, instead of letting the bot answer "not attached" forever.
|
|
@@ -5353,7 +5369,13 @@ export function apply(ctx, config) {
|
|
|
5353
5369
|
await sendTo(chat, 0, `the runtime's workspace registry did not list ${String(healed.restored)} workspace(s) / ${String(healed.attached)} session(s) that Telegram still had — they are back, and the web UI lists them again.`, 'control');
|
|
5354
5370
|
})
|
|
5355
5371
|
.catch((error) => stderr(`registry repair failed: ${String(error)}`));
|
|
5356
|
-
|
|
5372
|
+
started = true;
|
|
5373
|
+
if (botToken().trim().length === 0) {
|
|
5374
|
+
stderr('not configured: set the bot token on the Plugins page; polling starts when it is saved');
|
|
5375
|
+
}
|
|
5376
|
+
else {
|
|
5377
|
+
ensurePolling();
|
|
5378
|
+
}
|
|
5357
5379
|
void announceTrial();
|
|
5358
5380
|
void flushBridgeTurns().catch((error) => stderr(`bridge flush failed: ${String(error)}`));
|
|
5359
5381
|
void replayBridgeInputs().catch((error) => stderr(`bridge replay failed: ${String(error)}`));
|
|
@@ -5732,6 +5754,22 @@ export function apply(ctx, config) {
|
|
|
5732
5754
|
return;
|
|
5733
5755
|
}
|
|
5734
5756
|
});
|
|
5757
|
+
// A volatile-only config commit updates the running fiber in place; it does
|
|
5758
|
+
// not reload this plugin. Follow the values that affect the long-lived bot:
|
|
5759
|
+
// a new token changes the client and starts polling if it was idle.
|
|
5760
|
+
ctx.on('loader/volatile-update', (paths) => {
|
|
5761
|
+
const touched = (field) => paths.some((path) => path[path.length - 1] === field);
|
|
5762
|
+
if (touched('botToken')) {
|
|
5763
|
+
const next = botToken().trim();
|
|
5764
|
+
client.setToken(next);
|
|
5765
|
+
stderr(next.length > 0 ? 'bot token updated; polling with the new token' : 'bot token cleared; polling will fail until one is set');
|
|
5766
|
+
ensurePolling();
|
|
5767
|
+
}
|
|
5768
|
+
if (touched('allowedUserIds'))
|
|
5769
|
+
stderr(`allowed users updated (${String(allowedUserIds().length)})`);
|
|
5770
|
+
if (touched('adminUserId'))
|
|
5771
|
+
stderr(`admin updated (${adminUserId() === undefined ? 'first allowed user' : String(adminUserId())})`);
|
|
5772
|
+
});
|
|
5735
5773
|
ctx.effect(() => {
|
|
5736
5774
|
void start();
|
|
5737
5775
|
return () => {
|