@excitedjs/dreamux 0.1.1
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/README.md +223 -0
- package/bin/dreamux +31 -0
- package/bin/server +35 -0
- package/bin/server-ctl +28 -0
- package/db/migrations/0001_init.sql +49 -0
- package/dist/admin/methods.js +103 -0
- package/dist/admin/methods.js.map +1 -0
- package/dist/admin/protocol.js +15 -0
- package/dist/admin/protocol.js.map +1 -0
- package/dist/admin/socket.js +251 -0
- package/dist/admin/socket.js.map +1 -0
- package/dist/cli/dreamux.js +105 -0
- package/dist/cli/dreamux.js.map +1 -0
- package/dist/cli/server-ctl.js +172 -0
- package/dist/cli/server-ctl.js.map +1 -0
- package/dist/cli/server.js +88 -0
- package/dist/cli/server.js.map +1 -0
- package/dist/codex/events.js +82 -0
- package/dist/codex/events.js.map +1 -0
- package/dist/codex/handshake.js +85 -0
- package/dist/codex/handshake.js.map +1 -0
- package/dist/codex/rpc.js +200 -0
- package/dist/codex/rpc.js.map +1 -0
- package/dist/codex/supervisor.js +184 -0
- package/dist/codex/supervisor.js.map +1 -0
- package/dist/codex/types.js +10 -0
- package/dist/codex/types.js.map +1 -0
- package/dist/db/repository.js +207 -0
- package/dist/db/repository.js.map +1 -0
- package/dist/db/schema.js +29 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/db/types.js +2 -0
- package/dist/db/types.js.map +1 -0
- package/dist/dispatcher/approval.js +43 -0
- package/dist/dispatcher/approval.js.map +1 -0
- package/dist/dispatcher/runtime.js +262 -0
- package/dist/dispatcher/runtime.js.map +1 -0
- package/dist/dispatcher/turn-manager.js +167 -0
- package/dist/dispatcher/turn-manager.js.map +1 -0
- package/dist/feishu/bot.js +137 -0
- package/dist/feishu/bot.js.map +1 -0
- package/dist/feishu/content.js +108 -0
- package/dist/feishu/content.js.map +1 -0
- package/dist/feishu/render.js +600 -0
- package/dist/feishu/render.js.map +1 -0
- package/dist/feishu/types.js +9 -0
- package/dist/feishu/types.js.map +1 -0
- package/dist/runtime/codex-args.js +92 -0
- package/dist/runtime/codex-args.js.map +1 -0
- package/dist/runtime/config.js +351 -0
- package/dist/runtime/config.js.map +1 -0
- package/dist/runtime/paths.js +77 -0
- package/dist/runtime/paths.js.map +1 -0
- package/dist/runtime/secrets.js +18 -0
- package/dist/runtime/secrets.js.map +1 -0
- package/dist/server.js +234 -0
- package/dist/server.js.map +1 -0
- package/package.json +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 excitedjs
|
|
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/README.md
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# @excitedjs/dreamux
|
|
2
|
+
|
|
3
|
+
The Codex-host server package. One long-running Node process hosts N
|
|
4
|
+
**Dispatchers**; each Dispatcher binds **1 Feishu bot + 1 Codex thread**.
|
|
5
|
+
|
|
6
|
+
This file is the **package-level** quick start. For the monorepo layout
|
|
7
|
+
and harness pieces, see the top-level
|
|
8
|
+
[`README.md`](../../README.md) and
|
|
9
|
+
[`.agents/root.md`](../../.agents/root.md).
|
|
10
|
+
|
|
11
|
+
Design background:
|
|
12
|
+
[#1 Proposal](https://github.com/excitedjs/dreamux/issues/1) ·
|
|
13
|
+
[#2 Engineering plan](https://github.com/excitedjs/dreamux/issues/2) ·
|
|
14
|
+
[#4 Monorepo + harness](https://github.com/excitedjs/dreamux/issues/4).
|
|
15
|
+
|
|
16
|
+
## What this package ships
|
|
17
|
+
|
|
18
|
+
- The `dreamux` CLI (preferred): `dreamux server start`,
|
|
19
|
+
`dreamux server status`, `dreamux dispatcher add|remove|list|status|start|stop`.
|
|
20
|
+
- Legacy aliases: `dreamux-server` (= `dreamux server start`) and
|
|
21
|
+
`server-ctl` (= `dreamux <verb>`). Kept so pre-monorepo operators
|
|
22
|
+
don't have to rewrite PATH entries; see
|
|
23
|
+
[`.agents/decisions/0002-cli-and-package-naming.md`](../../.agents/decisions/0002-cli-and-package-naming.md).
|
|
24
|
+
- A SQLite-backed runtime (`dispatchers` + `inbound_buffer`) plus the
|
|
25
|
+
Feishu / Codex adapters that drive each dispatcher.
|
|
26
|
+
|
|
27
|
+
## What this MVP does (P0)
|
|
28
|
+
|
|
29
|
+
- **One Node process, many Dispatchers.** Each Dispatcher = 1 Feishu Bot
|
|
30
|
+
(independent appId/secret) + 1 long-lived Codex `app-server` child + 1
|
|
31
|
+
Codex thread.
|
|
32
|
+
- **Single-thread, multi-chat fan-in.** A bot can be invited into multiple
|
|
33
|
+
groups and DMs; every inbound message goes into the same Codex thread.
|
|
34
|
+
Outbound replies are routed by the inbound's `source_chat_id`.
|
|
35
|
+
- **No dispatcher↔worktree binding.** Codex picks the worktree at `tm`-call
|
|
36
|
+
time. The Codex daemon's cwd is `~/.codex-host/dispatchers/<id>/cwd/`
|
|
37
|
+
(intentionally empty).
|
|
38
|
+
- **FIFO + at-most-once.** One running turn per dispatcher. After a server
|
|
39
|
+
crash, `running` inbound rows are flipped to `unknown` (the user is told
|
|
40
|
+
to confirm or resend); `awaiting_outbound` rows are safely retried.
|
|
41
|
+
- **Trusted-local only.** No chat allowlist, `approval-policy=never`. Any
|
|
42
|
+
other deployment must uplift access control first — see
|
|
43
|
+
[issue #2 §"信任模型"](https://github.com/excitedjs/dreamux/issues/2).
|
|
44
|
+
|
|
45
|
+
Explicitly **not** in MVP: approval cards, streaming outbound, per-chat
|
|
46
|
+
threads, tm registry isolation, cross-machine coordination, web UI.
|
|
47
|
+
|
|
48
|
+
## Install / build / test
|
|
49
|
+
|
|
50
|
+
Two paths; both work, pick whichever fits the workflow.
|
|
51
|
+
|
|
52
|
+
**Per-package (this directory).** Matches pre-monorepo muscle memory and
|
|
53
|
+
runs the `prepare` hook (`tsc → dist/`):
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
cd packages/dreamux
|
|
57
|
+
npm install
|
|
58
|
+
npm run build # also runs automatically via `prepare`
|
|
59
|
+
npm run typecheck # tsc --noEmit (no emit)
|
|
60
|
+
npm test # smoke + bin-launcher + (live) codex-0134 tests
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Monorepo (from repo root).** Required once a second package exists; CI
|
|
64
|
+
exercises this path so a broken `rush.json` fails CI:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
node common/scripts/install-run-rush.js update
|
|
68
|
+
node common/scripts/install-run-rush.js build
|
|
69
|
+
node common/scripts/install-run-rush.js test
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The bin launchers shell out to plain `node` against the compiled `dist/`
|
|
73
|
+
output; **no `tsx` is needed at runtime** (PR #6).
|
|
74
|
+
|
|
75
|
+
## Run the server
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# Preferred — unified CLI (issue #4)
|
|
79
|
+
./bin/dreamux server start
|
|
80
|
+
|
|
81
|
+
# Backward-compat alias
|
|
82
|
+
./bin/server
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Both work from any cwd and via symlinks (PR #6 + bin-launcher tests).
|
|
86
|
+
|
|
87
|
+
The server uses two separate home directories — by design (decision 0003):
|
|
88
|
+
|
|
89
|
+
| Path | Purpose | Source of truth |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| `~/.dreamux/config.toml` | User-editable global config — auto-created on first boot with sensible defaults; edit and restart to apply | the operator |
|
|
92
|
+
| `~/.codex-host/state.db` | SQLite (dispatchers + inbound buffer) | the server |
|
|
93
|
+
| `~/.codex-host/admin.sock` | Admin Unix socket (`0600`) | the server |
|
|
94
|
+
| `~/.codex-host/dispatchers/<id>/cwd/` | Codex app-server cwd | the server |
|
|
95
|
+
| `~/.codex-host/dispatchers/<id>/socket` | Codex Unix socket | the server |
|
|
96
|
+
| `~/.codex-host/dispatchers/<id>/*.log` | Codex stdout / stderr | the server |
|
|
97
|
+
|
|
98
|
+
`rm -rf ~/.codex-host` is a safe recovery — your config in `~/.dreamux/`
|
|
99
|
+
survives. `runtime_dir` and `admin_socket` paths in the config can move
|
|
100
|
+
the `~/.codex-host` half anywhere you like.
|
|
101
|
+
|
|
102
|
+
## Configure a dispatcher
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# Bot secret comes from an env var the server process can see.
|
|
106
|
+
export BOT_SECRET_FLOW='cli_secret_XXX'
|
|
107
|
+
|
|
108
|
+
./bin/dreamux dispatcher add \
|
|
109
|
+
--id flow \
|
|
110
|
+
--bot-app-id cli_aaa \
|
|
111
|
+
--bot-secret-ref env:BOT_SECRET_FLOW
|
|
112
|
+
|
|
113
|
+
# Inspect / restart
|
|
114
|
+
./bin/dreamux dispatcher list
|
|
115
|
+
./bin/dreamux dispatcher status --id flow
|
|
116
|
+
./bin/dreamux dispatcher start --id flow # if not auto-started
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`./bin/server-ctl <args>` still works as an alias.
|
|
120
|
+
|
|
121
|
+
## MVP verification path (issue #2 §"MVP 验收脚本")
|
|
122
|
+
|
|
123
|
+
1. `dreamux dispatcher add --id flow --bot-app-id cli_aaa --bot-secret-ref env:BOT_SECRET_FLOW`
|
|
124
|
+
2. `dreamux server start` — dispatcher `flow` goes to `ready`
|
|
125
|
+
3. Invite the bot to a Feishu group A, send `hi`
|
|
126
|
+
4. Server delivers it into the Codex thread; reply goes back to group A
|
|
127
|
+
5. Invite the same bot to a DM, ask "do you remember the 'hi' from earlier?"
|
|
128
|
+
6. Same thread, so the reply confirms — and goes back to the DM
|
|
129
|
+
7. Ask the bot to "run the test suite via tm and summarize"
|
|
130
|
+
8. Codex shells out to `tm`, reads stdout/stderr, replies into the source chat
|
|
131
|
+
9. Repeat with a **different** worktree to prove dispatcher↔worktree decoupling
|
|
132
|
+
10. `pkill node` to crash the server, then restart it
|
|
133
|
+
11. Continue chatting — Codex `thread/resume` restores context
|
|
134
|
+
|
|
135
|
+
## Configuration reference
|
|
136
|
+
|
|
137
|
+
Precedence for every config-able value (highest wins): env var →
|
|
138
|
+
per-dispatcher field → `~/.dreamux/config.toml` → built-in default.
|
|
139
|
+
See [decision 0003](../../.agents/decisions/0003-global-config-dir.md).
|
|
140
|
+
|
|
141
|
+
### Global: `~/.dreamux/config.toml`
|
|
142
|
+
|
|
143
|
+
Auto-created on first boot with this default (excerpt — open the file
|
|
144
|
+
itself for the inline comments explaining each key):
|
|
145
|
+
|
|
146
|
+
```toml
|
|
147
|
+
runtime_dir = "~/.codex-host"
|
|
148
|
+
# admin_socket = "~/.codex-host/admin.sock" # default: <runtime_dir>/admin.sock
|
|
149
|
+
|
|
150
|
+
[codex]
|
|
151
|
+
bin = "codex"
|
|
152
|
+
approval_policy = "never" # never | auto | auto-approve | on-failure
|
|
153
|
+
sandbox_mode = "workspace-write" # read-only | workspace-write | danger-full-access
|
|
154
|
+
extra_args = []
|
|
155
|
+
initialize_timeout_ms = 10000
|
|
156
|
+
|
|
157
|
+
[outbound]
|
|
158
|
+
retries = 3
|
|
159
|
+
retry_delay_ms = 1000
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Edit and restart `dreamux server start`. Parse errors fail-fast with a
|
|
163
|
+
`file:line` pointer.
|
|
164
|
+
|
|
165
|
+
### `codex_args_json` (per-dispatcher, overrides global)
|
|
166
|
+
|
|
167
|
+
JSON object stored in `dispatchers.codex_args_json`:
|
|
168
|
+
|
|
169
|
+
```json
|
|
170
|
+
{ "approvalPolicy": "never", "sandboxMode": "workspace-write", "extraArgs": ["--model", "gpt-5"] }
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
| Field | Default | Notes |
|
|
174
|
+
| ---------------- | --------- | ---------------------------------------------------- |
|
|
175
|
+
| `approvalPolicy` | inherits `[codex] approval_policy` from `~/.dreamux/config.toml`, else `"never"` | Must be one of `never`/`auto`/`auto-approve`/`on-failure`. Otherwise startup fails fast (issue #2 §"实现陷阱"). |
|
|
176
|
+
| `sandboxMode` | inherits `[codex] sandbox_mode`, else `"workspace-write"` | Must be one of `read-only`/`workspace-write`/`danger-full-access` (codex 0.134 enum). Validated at dispatcher startup. |
|
|
177
|
+
| `extraArgs` | appended *after* global `codex.extra_args` | codex's "last write wins" semantics for `-c key=value` mean a per-dispatcher entry effectively overrides a same-key global. |
|
|
178
|
+
|
|
179
|
+
### Env vars (highest precedence — escape hatch)
|
|
180
|
+
|
|
181
|
+
| Var | Purpose |
|
|
182
|
+
| ---------------------------- | -------------------------------------------------- |
|
|
183
|
+
| `CODEX_HOST_RUNTIME_DIR` | Override `runtime_dir` |
|
|
184
|
+
| `CODEX_HOST_ADMIN_SOCKET` | Override admin Unix socket path |
|
|
185
|
+
| `CODEX_HOST_CODEX_BIN` | Override `codex.bin` |
|
|
186
|
+
| `DREAMUX_CONFIG_DIR` | Override `~/.dreamux` (where `config.toml` lives) |
|
|
187
|
+
| `BOT_SECRET_<NAME>` | Bot secrets referenced by `env:BOT_SECRET_<NAME>` |
|
|
188
|
+
| `DREAMUX_SKIP_LIVE_CODEX` | Opt out of the live codex 0.134 integration test (loud skip) |
|
|
189
|
+
|
|
190
|
+
## What this MVP does **not** do
|
|
191
|
+
|
|
192
|
+
(see [issue #2 §"明确不在 MVP 范围"](https://github.com/excitedjs/dreamux/issues/2))
|
|
193
|
+
|
|
194
|
+
- Multiple threads per dispatcher
|
|
195
|
+
- Per-chat memory
|
|
196
|
+
- Approval / Feishu approval cards
|
|
197
|
+
- Streaming assistant deltas
|
|
198
|
+
- tm CLI changes (`tm --json`, registry namespace)
|
|
199
|
+
- Cross-machine coordination
|
|
200
|
+
- Web UI / Prometheus
|
|
201
|
+
- Migration from old claudemux dispatcher state
|
|
202
|
+
- access gate / chat allowlist / pairing (D12 + Trust Model)
|
|
203
|
+
|
|
204
|
+
## Testing
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
npm test # smoke + bin-launcher + codex-0134-live
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
- `tests/smoke.test.ts` — fake-codex-driven dispatcher behavior:
|
|
211
|
+
happy path, FIFO, crash recovery (running → unknown), thread/resume
|
|
212
|
+
failure, outbound retry without turn re-run, approval fail-fast.
|
|
213
|
+
- `tests/bin-launcher.test.ts` — spawns the real bash launchers
|
|
214
|
+
(`dreamux`, `dreamux-server`, `server-ctl`, plus the repo-root
|
|
215
|
+
forwarders) from arbitrary cwds and through symlinks; static "no tsx"
|
|
216
|
+
assertion.
|
|
217
|
+
- `tests/codex-0134-live.test.ts` — spawns a real `codex app-server`
|
|
218
|
+
(skipped loudly when `codex` is missing or wrong version; opt-in via
|
|
219
|
+
`DREAMUX_SKIP_LIVE_CODEX=1`).
|
|
220
|
+
|
|
221
|
+
## License
|
|
222
|
+
|
|
223
|
+
MIT.
|
package/bin/dreamux
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Launcher for the unified `dreamux` CLI (issue #4).
|
|
3
|
+
# Resolves its own location through symlinks so it works from any cwd and
|
|
4
|
+
# via any chain of symlinks (npm-link, ~/bin shortcuts, etc.). Runs the
|
|
5
|
+
# compiled dist output produced by `npm install` / `rush build`.
|
|
6
|
+
set -eu
|
|
7
|
+
|
|
8
|
+
SOURCE="${BASH_SOURCE[0]}"
|
|
9
|
+
hop=0
|
|
10
|
+
while [ -L "$SOURCE" ]; do
|
|
11
|
+
hop=$((hop + 1))
|
|
12
|
+
if [ "$hop" -gt 40 ]; then
|
|
13
|
+
echo "dreamux: too many symlink hops resolving $SOURCE" >&2
|
|
14
|
+
exit 1
|
|
15
|
+
fi
|
|
16
|
+
TARGET="$(readlink "$SOURCE")"
|
|
17
|
+
if [ "${TARGET:0:1}" = "/" ]; then
|
|
18
|
+
SOURCE="$TARGET"
|
|
19
|
+
else
|
|
20
|
+
SOURCE="$(dirname -- "$SOURCE")/$TARGET"
|
|
21
|
+
fi
|
|
22
|
+
done
|
|
23
|
+
|
|
24
|
+
DIR="$(cd -- "$(dirname -- "$SOURCE")/.." &> /dev/null && pwd)"
|
|
25
|
+
TARGET="$DIR/dist/cli/dreamux.js"
|
|
26
|
+
if [ ! -f "$TARGET" ]; then
|
|
27
|
+
echo "dreamux: $TARGET is missing." >&2
|
|
28
|
+
echo "Run 'npm install' (it runs 'npm run build' via the prepare hook), 'npm run build', or 'rush build' from the monorepo root first." >&2
|
|
29
|
+
exit 1
|
|
30
|
+
fi
|
|
31
|
+
exec node "$TARGET" "$@"
|
package/bin/server
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Launcher for dreamux-server.
|
|
3
|
+
#
|
|
4
|
+
# Resolves its own location (following symlinks) so it works from any cwd
|
|
5
|
+
# AND when invoked via a symlink (e.g. `npm install -g` puts a symlink under
|
|
6
|
+
# the node bin dir). Runs the compiled dist output produced by
|
|
7
|
+
# `npm install` / `npm run build`.
|
|
8
|
+
set -eu
|
|
9
|
+
|
|
10
|
+
SOURCE="${BASH_SOURCE[0]}"
|
|
11
|
+
# Resolve symlinks until we find the real file. Handles both absolute and
|
|
12
|
+
# relative symlink targets. Bounded by a reasonable loop cap to detect cycles.
|
|
13
|
+
hop=0
|
|
14
|
+
while [ -L "$SOURCE" ]; do
|
|
15
|
+
hop=$((hop + 1))
|
|
16
|
+
if [ "$hop" -gt 40 ]; then
|
|
17
|
+
echo "dreamux-server: too many symlink hops resolving $SOURCE" >&2
|
|
18
|
+
exit 1
|
|
19
|
+
fi
|
|
20
|
+
TARGET="$(readlink "$SOURCE")"
|
|
21
|
+
if [ "${TARGET:0:1}" = "/" ]; then
|
|
22
|
+
SOURCE="$TARGET"
|
|
23
|
+
else
|
|
24
|
+
SOURCE="$(dirname -- "$SOURCE")/$TARGET"
|
|
25
|
+
fi
|
|
26
|
+
done
|
|
27
|
+
|
|
28
|
+
DIR="$(cd -- "$(dirname -- "$SOURCE")/.." &> /dev/null && pwd)"
|
|
29
|
+
TARGET="$DIR/dist/cli/server.js"
|
|
30
|
+
if [ ! -f "$TARGET" ]; then
|
|
31
|
+
echo "dreamux-server: $TARGET is missing." >&2
|
|
32
|
+
echo "Run 'npm install' (it runs 'npm run build' via the prepare hook) or 'npm run build' from $DIR first." >&2
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
exec node "$TARGET" "$@"
|
package/bin/server-ctl
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Launcher for server-ctl. See bin/server for resolution details.
|
|
3
|
+
set -eu
|
|
4
|
+
|
|
5
|
+
SOURCE="${BASH_SOURCE[0]}"
|
|
6
|
+
hop=0
|
|
7
|
+
while [ -L "$SOURCE" ]; do
|
|
8
|
+
hop=$((hop + 1))
|
|
9
|
+
if [ "$hop" -gt 40 ]; then
|
|
10
|
+
echo "server-ctl: too many symlink hops resolving $SOURCE" >&2
|
|
11
|
+
exit 1
|
|
12
|
+
fi
|
|
13
|
+
TARGET="$(readlink "$SOURCE")"
|
|
14
|
+
if [ "${TARGET:0:1}" = "/" ]; then
|
|
15
|
+
SOURCE="$TARGET"
|
|
16
|
+
else
|
|
17
|
+
SOURCE="$(dirname -- "$SOURCE")/$TARGET"
|
|
18
|
+
fi
|
|
19
|
+
done
|
|
20
|
+
|
|
21
|
+
DIR="$(cd -- "$(dirname -- "$SOURCE")/.." &> /dev/null && pwd)"
|
|
22
|
+
TARGET="$DIR/dist/cli/server-ctl.js"
|
|
23
|
+
if [ ! -f "$TARGET" ]; then
|
|
24
|
+
echo "server-ctl: $TARGET is missing." >&2
|
|
25
|
+
echo "Run 'npm install' (it runs 'npm run build' via the prepare hook) or 'npm run build' from $DIR first." >&2
|
|
26
|
+
exit 1
|
|
27
|
+
fi
|
|
28
|
+
exec node "$TARGET" "$@"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
-- dreamux MVP schema (issue excitedjs/dreamux#2)
|
|
2
|
+
-- Version tracked via PRAGMA user_version, not a schema_version table.
|
|
3
|
+
|
|
4
|
+
CREATE TABLE dispatchers (
|
|
5
|
+
dispatcher_id TEXT PRIMARY KEY,
|
|
6
|
+
bot_app_id TEXT NOT NULL UNIQUE,
|
|
7
|
+
bot_secret_ref TEXT NOT NULL,
|
|
8
|
+
codex_args_json TEXT NOT NULL DEFAULT '{}',
|
|
9
|
+
codex_cwd TEXT,
|
|
10
|
+
thread_id TEXT,
|
|
11
|
+
status TEXT NOT NULL DEFAULT 'declared'
|
|
12
|
+
CHECK (status IN ('declared','starting','ready','degraded','stopping','stopped')),
|
|
13
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
14
|
+
created_at INTEGER NOT NULL,
|
|
15
|
+
updated_at INTEGER NOT NULL,
|
|
16
|
+
last_started_at INTEGER,
|
|
17
|
+
last_ready_at INTEGER,
|
|
18
|
+
last_error TEXT,
|
|
19
|
+
last_lost_thread_id TEXT
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
CREATE TABLE inbound_buffer (
|
|
23
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
24
|
+
dispatcher_id TEXT NOT NULL REFERENCES dispatchers(dispatcher_id),
|
|
25
|
+
source_chat_id TEXT NOT NULL,
|
|
26
|
+
source_message_id TEXT,
|
|
27
|
+
sender_id TEXT,
|
|
28
|
+
feishu_event_json TEXT NOT NULL,
|
|
29
|
+
parsed_text TEXT NOT NULL,
|
|
30
|
+
state TEXT NOT NULL DEFAULT 'queued'
|
|
31
|
+
CHECK (state IN ('queued','running','awaiting_outbound',
|
|
32
|
+
'completed','outbound_failed','failed','unknown')),
|
|
33
|
+
codex_turn_id TEXT,
|
|
34
|
+
assistant_text TEXT,
|
|
35
|
+
feishu_message_ids_json TEXT,
|
|
36
|
+
outbound_error TEXT,
|
|
37
|
+
received_at INTEGER NOT NULL,
|
|
38
|
+
started_at INTEGER,
|
|
39
|
+
completed_at INTEGER,
|
|
40
|
+
failed_at INTEGER,
|
|
41
|
+
error TEXT
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
CREATE INDEX idx_inbound_dispatcher_state
|
|
45
|
+
ON inbound_buffer(dispatcher_id, state, id);
|
|
46
|
+
|
|
47
|
+
CREATE UNIQUE INDEX idx_inbound_message_dedupe
|
|
48
|
+
ON inbound_buffer(dispatcher_id, source_message_id)
|
|
49
|
+
WHERE source_message_id IS NOT NULL;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin method handlers.
|
|
3
|
+
*
|
|
4
|
+
* Each handler takes typed params and returns the `result` payload to put on
|
|
5
|
+
* the wire. Throws `AdminError` for user-actionable failures (the protocol
|
|
6
|
+
* layer formats those as `error` responses).
|
|
7
|
+
*/
|
|
8
|
+
import { AdminError } from './protocol.js';
|
|
9
|
+
export const adminMethods = {
|
|
10
|
+
'server.status': (server) => ({
|
|
11
|
+
pid: process.pid,
|
|
12
|
+
uptimeSec: Math.floor(process.uptime()),
|
|
13
|
+
dispatchers: server.summarize(),
|
|
14
|
+
}),
|
|
15
|
+
'dispatcher.add': (server, params) => {
|
|
16
|
+
const id = mustString(params, 'dispatcher_id');
|
|
17
|
+
const botAppId = mustString(params, 'bot_app_id');
|
|
18
|
+
const botSecretRef = mustString(params, 'bot_secret_ref');
|
|
19
|
+
const codexArgsJson = optionalString(params, 'codex_args_json') ?? '{}';
|
|
20
|
+
const codexCwd = optionalString(params, 'codex_cwd');
|
|
21
|
+
try {
|
|
22
|
+
const row = server.repos.dispatchers.create({
|
|
23
|
+
dispatcher_id: id,
|
|
24
|
+
bot_app_id: botAppId,
|
|
25
|
+
bot_secret_ref: botSecretRef,
|
|
26
|
+
codex_args_json: codexArgsJson,
|
|
27
|
+
codex_cwd: codexCwd ?? null,
|
|
28
|
+
});
|
|
29
|
+
return { dispatcher_id: row.dispatcher_id, status: row.status };
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
if (err && typeof err === 'object') {
|
|
33
|
+
const code = err.code;
|
|
34
|
+
if (code === 'SQLITE_CONSTRAINT_UNIQUE' ||
|
|
35
|
+
code === 'SQLITE_CONSTRAINT_PRIMARYKEY') {
|
|
36
|
+
throw new AdminError('CONFLICT', `dispatcher_id or bot_app_id already exists: ${err.message}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
throw err;
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
'dispatcher.remove': async (server, params) => {
|
|
43
|
+
const id = mustString(params, 'dispatcher_id');
|
|
44
|
+
const row = server.repos.dispatchers.get(id);
|
|
45
|
+
if (row === null) {
|
|
46
|
+
throw new AdminError('DISPATCHER_NOT_FOUND', `no dispatcher with id '${id}'`);
|
|
47
|
+
}
|
|
48
|
+
await server.stopDispatcher(id);
|
|
49
|
+
server.repos.dispatchers.remove(id);
|
|
50
|
+
return { dispatcher_id: id };
|
|
51
|
+
},
|
|
52
|
+
'dispatcher.list': (server) => ({ dispatchers: server.summarize() }),
|
|
53
|
+
'dispatcher.status': (server, params) => {
|
|
54
|
+
const id = mustString(params, 'dispatcher_id');
|
|
55
|
+
const row = server.repos.dispatchers.get(id);
|
|
56
|
+
if (row === null) {
|
|
57
|
+
throw new AdminError('DISPATCHER_NOT_FOUND', `no dispatcher with id '${id}'`);
|
|
58
|
+
}
|
|
59
|
+
const runtime = server.getRuntime(id);
|
|
60
|
+
const counts = server.repos.inbound.countByState(id);
|
|
61
|
+
return {
|
|
62
|
+
dispatcher_id: row.dispatcher_id,
|
|
63
|
+
bot_app_id: row.bot_app_id,
|
|
64
|
+
status: runtime?.getStatus() ?? row.status,
|
|
65
|
+
thread_id: runtime?.getThreadId() ?? row.thread_id,
|
|
66
|
+
last_lost_thread_id: row.last_lost_thread_id,
|
|
67
|
+
last_error: row.last_error,
|
|
68
|
+
inbound_buffer: counts,
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
'dispatcher.start': async (server, params) => {
|
|
72
|
+
const id = mustString(params, 'dispatcher_id');
|
|
73
|
+
const row = server.repos.dispatchers.get(id);
|
|
74
|
+
if (row === null) {
|
|
75
|
+
throw new AdminError('DISPATCHER_NOT_FOUND', `no dispatcher with id '${id}'`);
|
|
76
|
+
}
|
|
77
|
+
await server.startDispatcher(id);
|
|
78
|
+
return { dispatcher_id: id, status: server.getRuntime(id)?.getStatus() };
|
|
79
|
+
},
|
|
80
|
+
'dispatcher.stop': async (server, params) => {
|
|
81
|
+
const id = mustString(params, 'dispatcher_id');
|
|
82
|
+
await server.stopDispatcher(id);
|
|
83
|
+
return { dispatcher_id: id, status: 'stopped' };
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
function mustString(params, key) {
|
|
87
|
+
if (params === undefined || typeof params[key] !== 'string') {
|
|
88
|
+
throw new AdminError('BAD_REQUEST', `missing or non-string param '${key}'`);
|
|
89
|
+
}
|
|
90
|
+
return params[key];
|
|
91
|
+
}
|
|
92
|
+
function optionalString(params, key) {
|
|
93
|
+
if (params === undefined)
|
|
94
|
+
return null;
|
|
95
|
+
const v = params[key];
|
|
96
|
+
if (v === undefined || v === null)
|
|
97
|
+
return null;
|
|
98
|
+
if (typeof v !== 'string') {
|
|
99
|
+
throw new AdminError('BAD_REQUEST', `param '${key}' must be a string`);
|
|
100
|
+
}
|
|
101
|
+
return v;
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=methods.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"methods.js","sourceRoot":"","sources":["../../src/admin/methods.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NDJSON Unix-socket protocol for admin commands (issue #2 §"管理接口").
|
|
3
|
+
*
|
|
4
|
+
* One line in / one line out. Permissions on the socket are 0600 (only the
|
|
5
|
+
* owner). Method names use dotted lowercase; error codes use SCREAMING_SNAKE_CASE.
|
|
6
|
+
*/
|
|
7
|
+
export class AdminError extends Error {
|
|
8
|
+
code;
|
|
9
|
+
constructor(code, message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.code = code;
|
|
12
|
+
this.name = 'AdminError';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=protocol.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../src/admin/protocol.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAsBH,MAAM,OAAO,UAAW,SAAQ,KAAK;IACP;IAA5B,YAA4B,IAAY,EAAE,OAAe;QACvD,KAAK,CAAC,OAAO,CAAC,CAAC;QADW,SAAI,GAAJ,IAAI,CAAQ;QAEtC,IAAI,CAAC,IAAI,GAAG,YAAY,CAAC;IAC3B,CAAC;CACF"}
|