dsh-telegram-connector 0.14.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 +37 -0
- package/COMPATIBILITY.md +2 -0
- package/README.md +23 -5
- package/docs/DESIGN.md +3 -1
- package/docs/STANDALONE.md +9 -4
- package/lib/client.js +244 -0
- 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 +18 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
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
|
+
|
|
21
|
+
## 0.15.1
|
|
22
|
+
|
|
23
|
+
- Release-check fix: the `0.15.0` tag was cut without its COMPATIBILITY row and
|
|
24
|
+
was never published; this is the published release of the configuration page
|
|
25
|
+
described below.
|
|
26
|
+
|
|
27
|
+
## 0.15.0
|
|
28
|
+
|
|
29
|
+
- **The connector has a configuration page.** On DSH `0.2.1-rc.5` or newer, the
|
|
30
|
+
Plugins page shows **Configure** on the `dsh-telegram-connector` row: bot
|
|
31
|
+
token, allowed user ids and admin user id, saved through the page's own form.
|
|
32
|
+
No `.env` edit or restart is needed; the connector recomposes and starts.
|
|
33
|
+
- **Standalone is a first-class mode.** The setup is optional: the connector
|
|
34
|
+
does not assume the setup supervisor or updater. The setup-specific commands
|
|
35
|
+
report the missing sidecar instead of failing; workspaces, sessions, topics,
|
|
36
|
+
plugins and model calls work on any DSH runtime.
|
|
37
|
+
- The browser half is plain JavaScript in the client-module format, copied to
|
|
38
|
+
`lib/client.js` by the build; the host half is unchanged.
|
|
39
|
+
|
|
3
40
|
## 0.14.1
|
|
4
41
|
|
|
5
42
|
- **Component actions no longer restart the runtime.** The setup's
|
package/COMPATIBILITY.md
CHANGED
|
@@ -6,6 +6,8 @@ every release.
|
|
|
6
6
|
|
|
7
7
|
| Connector | DSH | Status | Notes |
|
|
8
8
|
|---|---|---|---|
|
|
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 |
|
|
9
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 |
|
|
10
12
|
| 0.14.0 | `0.2.1-rc.5` | needs UAT | one command surface: `/menu` card, `/plugins` subcommands, `/components`; aliases and `/man` removed |
|
|
11
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
|
@@ -141,11 +141,11 @@ Telegram:
|
|
|
141
141
|
```sh
|
|
142
142
|
git clone https://github.com/vladtsvirko/dsh-tg-docker-setup
|
|
143
143
|
cd dsh-tg-docker-setup
|
|
144
|
-
./
|
|
144
|
+
./setup.sh # asks for port, bind address, provider, key,
|
|
145
145
|
# bot token, allowed ids, admin id, git author
|
|
146
146
|
```
|
|
147
147
|
|
|
148
|
-
`./
|
|
148
|
+
`./setup.sh --check` prints the whole plan and changes nothing. The setup
|
|
149
149
|
pins a recommended connector tag in its `plugins.json`; the connector is
|
|
150
150
|
versioned on its own and can move to a newer compatible version from the bot without
|
|
151
151
|
a setup release. See [`docs/STANDALONE.md`](docs/STANDALONE.md) for the same
|
|
@@ -154,8 +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.
|
|
158
|
-
export DSH_TELEGRAM_BOT_TOKEN='123456:ABC…'
|
|
157
|
+
dsh plugin --profile web add dsh-telegram-connector@0.15.2
|
|
159
158
|
```
|
|
160
159
|
|
|
161
160
|
The registry tarball already contains the built `lib/`, so there is no
|
|
@@ -163,6 +162,22 @@ The registry tarball already contains the built `lib/`, so there is no
|
|
|
163
162
|
the plugin. The setup's `plugins.json` uses the same spec and keeps the GitHub
|
|
164
163
|
tag as a fallback when the registry is unreachable.
|
|
165
164
|
|
|
165
|
+
### Configure in the UI
|
|
166
|
+
|
|
167
|
+
On DSH `0.2.1-rc.5` or newer, open **Plugins** → **dsh-telegram-connector** and
|
|
168
|
+
press **Configure** on the `dsh-telegram-connector` row. The page has three
|
|
169
|
+
fields — **bot token**, **allowed user ids** and **admin user id** — and
|
|
170
|
+
**Save** writes the row's config into the profile's user layer. No `.env` edit
|
|
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.
|
|
173
|
+
|
|
174
|
+
That is the standalone path: the setup is not required, and the connector does
|
|
175
|
+
not assume the setup's supervisor or updater. Commands that need them
|
|
176
|
+
(`/system update setup`, `/components`, `/firewall allow`, restart/rollback)
|
|
177
|
+
report that the sidecar is unavailable instead of failing. Everything else —
|
|
178
|
+
workspaces, sessions, topics, plugins and model calls — works on any DSH
|
|
179
|
+
runtime.
|
|
180
|
+
|
|
166
181
|
Installing from the source repository is still possible:
|
|
167
182
|
|
|
168
183
|
```sh
|
|
@@ -170,7 +185,7 @@ cat >> "$DSH_HOME/profiles/web/pnpm-workspace.yaml" <<'YAML'
|
|
|
170
185
|
allowBuilds:
|
|
171
186
|
dsh-telegram-connector@https://github.com/vladtsvirko/dsh-telegram-connector.git: true
|
|
172
187
|
YAML
|
|
173
|
-
dsh plugin --profile web add github:vladtsvirko/dsh-telegram-connector#v0.
|
|
188
|
+
dsh plugin --profile web add github:vladtsvirko/dsh-telegram-connector#v0.15.2
|
|
174
189
|
```
|
|
175
190
|
|
|
176
191
|
That path runs the package's `prepare` build; pnpm ≥ 10 refuses it until the
|
|
@@ -180,6 +195,9 @@ The package is a plugin bundle: `cordis.patch.yml` mounts one row.
|
|
|
180
195
|
|
|
181
196
|
## Configuration
|
|
182
197
|
|
|
198
|
+
The row config from the UI is the first source. The environment is the
|
|
199
|
+
fallback (the setup writes it; a standalone profile can export it instead):
|
|
200
|
+
|
|
183
201
|
Environment:
|
|
184
202
|
|
|
185
203
|
| Variable | Meaning |
|
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
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser half: the configuration page for the `dsh-telegram-connector` row.
|
|
3
|
+
*
|
|
4
|
+
* DSH's Plugins page declares the `plugins.row.config` slot; this module
|
|
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
|
+
* profile's user layer, which wins over it.
|
|
9
|
+
*/
|
|
10
|
+
window.__ModuleLoader__.load({
|
|
11
|
+
id: 'dsh-telegram-connector',
|
|
12
|
+
factory: (require) => {
|
|
13
|
+
var module = { exports: {} }
|
|
14
|
+
var exports = module.exports
|
|
15
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
|
|
16
|
+
|
|
17
|
+
const React = require('react')
|
|
18
|
+
const h = React.createElement
|
|
19
|
+
|
|
20
|
+
const KEY = 'dsh-telegram-connector#dsh-telegram-connector'
|
|
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
|
+
`
|
|
40
|
+
|
|
41
|
+
function Field({ label, hint, children }) {
|
|
42
|
+
return h(
|
|
43
|
+
'label',
|
|
44
|
+
{ className: 'field' },
|
|
45
|
+
h('span', { className: 'label' }, label),
|
|
46
|
+
hint === undefined ? null : h('span', { className: 'hint' }, hint),
|
|
47
|
+
children,
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseIds(text) {
|
|
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
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function TelegramConfigForm({ form }) {
|
|
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) ?? {}
|
|
67
|
+
const [botToken, setBotToken] = React.useState(String(value.botToken ?? ''))
|
|
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
|
+
)
|
|
74
|
+
const [saving, setSaving] = React.useState(false)
|
|
75
|
+
const [message, setMessage] = React.useState('')
|
|
76
|
+
|
|
77
|
+
const revision = state === undefined ? undefined : state.revision
|
|
78
|
+
React.useEffect(() => {
|
|
79
|
+
const next = state === undefined ? {} : (state.value ?? {})
|
|
80
|
+
setBotToken(String(next.botToken ?? ''))
|
|
81
|
+
setAllowed(Array.isArray(next.allowedUserIds) ? next.allowedUserIds.join(', ') : '')
|
|
82
|
+
setAdmin(next.adminUserId === undefined ? '' : String(next.adminUserId))
|
|
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
|
+
}
|
|
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
|
|
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
|
+
|
|
104
|
+
const save = async () => {
|
|
105
|
+
const ids = parseIds(allowed)
|
|
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.')
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
setSaving(true)
|
|
117
|
+
setMessage('')
|
|
118
|
+
try {
|
|
119
|
+
const ops = [
|
|
120
|
+
{ op: 'set', path: ['botToken'], value: botToken.trim() },
|
|
121
|
+
{ op: 'set', path: ['allowedUserIds'], value: ids },
|
|
122
|
+
adminId === undefined
|
|
123
|
+
? { op: 'unset', path: ['adminUserId'] }
|
|
124
|
+
: { op: 'set', path: ['adminUserId'], value: adminId },
|
|
125
|
+
]
|
|
126
|
+
const ok = await form.mutate(ops, state.revision)
|
|
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
|
+
}
|
|
134
|
+
} catch (error) {
|
|
135
|
+
setMessage(`Save failed: ${String(error)}`)
|
|
136
|
+
} finally {
|
|
137
|
+
setSaving(false)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return h(
|
|
142
|
+
'div',
|
|
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
|
+
},
|
|
172
|
+
h('input', {
|
|
173
|
+
type: 'password',
|
|
174
|
+
className: 'input',
|
|
175
|
+
value: botToken,
|
|
176
|
+
disabled,
|
|
177
|
+
placeholder: '123456789:AA…',
|
|
178
|
+
onChange: (event) => setBotToken(event.target.value),
|
|
179
|
+
}),
|
|
180
|
+
),
|
|
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
|
+
},
|
|
187
|
+
h('input', {
|
|
188
|
+
type: 'text',
|
|
189
|
+
className: 'input',
|
|
190
|
+
value: allowed,
|
|
191
|
+
disabled,
|
|
192
|
+
placeholder: '142283532, 777000111',
|
|
193
|
+
onChange: (event) => setAllowed(event.target.value),
|
|
194
|
+
}),
|
|
195
|
+
),
|
|
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
|
+
},
|
|
202
|
+
h('input', {
|
|
203
|
+
type: 'text',
|
|
204
|
+
className: 'input',
|
|
205
|
+
value: admin,
|
|
206
|
+
disabled,
|
|
207
|
+
placeholder: 'optional',
|
|
208
|
+
onChange: (event) => setAdmin(event.target.value),
|
|
209
|
+
}),
|
|
210
|
+
),
|
|
211
|
+
h(
|
|
212
|
+
'div',
|
|
213
|
+
{ className: 'actions' },
|
|
214
|
+
h(
|
|
215
|
+
'button',
|
|
216
|
+
{
|
|
217
|
+
type: 'button',
|
|
218
|
+
className: 'primary',
|
|
219
|
+
disabled,
|
|
220
|
+
onClick: save,
|
|
221
|
+
},
|
|
222
|
+
saving ? 'Saving…' : 'Save',
|
|
223
|
+
),
|
|
224
|
+
message.length === 0 ? null : h('span', { className: 'message' }, message),
|
|
225
|
+
),
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const inject = ['slots']
|
|
230
|
+
function apply(ctx) {
|
|
231
|
+
ctx.slots.inject('plugins.row.config', () =>
|
|
232
|
+
ctx.slots.register(
|
|
233
|
+
{ name: 'plugins.row.config', key: KEY },
|
|
234
|
+
({ view, form }) => (view === 'summary' ? null : h(TelegramConfigForm, { form })),
|
|
235
|
+
),
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
exports.NS = 'dsh-telegram-connector'
|
|
240
|
+
exports.apply = apply
|
|
241
|
+
exports.inject = inject
|
|
242
|
+
return module.exports
|
|
243
|
+
},
|
|
244
|
+
})
|
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 () => {
|