@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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +223 -0
  3. package/bin/dreamux +31 -0
  4. package/bin/server +35 -0
  5. package/bin/server-ctl +28 -0
  6. package/db/migrations/0001_init.sql +49 -0
  7. package/dist/admin/methods.js +103 -0
  8. package/dist/admin/methods.js.map +1 -0
  9. package/dist/admin/protocol.js +15 -0
  10. package/dist/admin/protocol.js.map +1 -0
  11. package/dist/admin/socket.js +251 -0
  12. package/dist/admin/socket.js.map +1 -0
  13. package/dist/cli/dreamux.js +105 -0
  14. package/dist/cli/dreamux.js.map +1 -0
  15. package/dist/cli/server-ctl.js +172 -0
  16. package/dist/cli/server-ctl.js.map +1 -0
  17. package/dist/cli/server.js +88 -0
  18. package/dist/cli/server.js.map +1 -0
  19. package/dist/codex/events.js +82 -0
  20. package/dist/codex/events.js.map +1 -0
  21. package/dist/codex/handshake.js +85 -0
  22. package/dist/codex/handshake.js.map +1 -0
  23. package/dist/codex/rpc.js +200 -0
  24. package/dist/codex/rpc.js.map +1 -0
  25. package/dist/codex/supervisor.js +184 -0
  26. package/dist/codex/supervisor.js.map +1 -0
  27. package/dist/codex/types.js +10 -0
  28. package/dist/codex/types.js.map +1 -0
  29. package/dist/db/repository.js +207 -0
  30. package/dist/db/repository.js.map +1 -0
  31. package/dist/db/schema.js +29 -0
  32. package/dist/db/schema.js.map +1 -0
  33. package/dist/db/types.js +2 -0
  34. package/dist/db/types.js.map +1 -0
  35. package/dist/dispatcher/approval.js +43 -0
  36. package/dist/dispatcher/approval.js.map +1 -0
  37. package/dist/dispatcher/runtime.js +262 -0
  38. package/dist/dispatcher/runtime.js.map +1 -0
  39. package/dist/dispatcher/turn-manager.js +167 -0
  40. package/dist/dispatcher/turn-manager.js.map +1 -0
  41. package/dist/feishu/bot.js +137 -0
  42. package/dist/feishu/bot.js.map +1 -0
  43. package/dist/feishu/content.js +108 -0
  44. package/dist/feishu/content.js.map +1 -0
  45. package/dist/feishu/render.js +600 -0
  46. package/dist/feishu/render.js.map +1 -0
  47. package/dist/feishu/types.js +9 -0
  48. package/dist/feishu/types.js.map +1 -0
  49. package/dist/runtime/codex-args.js +92 -0
  50. package/dist/runtime/codex-args.js.map +1 -0
  51. package/dist/runtime/config.js +351 -0
  52. package/dist/runtime/config.js.map +1 -0
  53. package/dist/runtime/paths.js +77 -0
  54. package/dist/runtime/paths.js.map +1 -0
  55. package/dist/runtime/secrets.js +18 -0
  56. package/dist/runtime/secrets.js.map +1 -0
  57. package/dist/server.js +234 -0
  58. package/dist/server.js.map +1 -0
  59. package/package.json +43 -0
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Per-dispatcher FIFO turn worker.
3
+ *
4
+ * Issue #2 §"Codex 协议处理": the queue worker is the single executor for one
5
+ * dispatcher's Codex thread. Holds an in-memory FIFO; pulls from
6
+ * `inbound_buffer` lazily so we never lose messages across crashes.
7
+ *
8
+ * State machine (transitions are the only ones this worker performs):
9
+ * queued
10
+ * └─[worker]→ running
11
+ * └─[turn/completed]→ awaiting_outbound
12
+ * ├─[feishu send OK]→ completed
13
+ * └─[feishu send fail]→ outbound_failed → retry → completed
14
+ * └─[turn/start RPC failure]→ failed
15
+ *
16
+ * Server crash recovery is handled separately by `recoverDispatcher()`:
17
+ * running → unknown (at-most-once, see issue #2 §"崩溃与异常恢复")
18
+ * awaiting_outbound → safe to retry the outbound
19
+ * outbound_failed → safe to retry the outbound
20
+ */
21
+ import { extractAssistantText, runTurn } from '../codex/events.js';
22
+ export class TurnManager {
23
+ opts;
24
+ running = false;
25
+ stopped = false;
26
+ wakeup = null;
27
+ log;
28
+ outboundRetries;
29
+ outboundRetryDelayMs;
30
+ emptyTurnPlaceholder;
31
+ constructor(opts) {
32
+ this.opts = opts;
33
+ this.log = opts.log ?? ((lvl, msg, err) => {
34
+ const prefix = `[turn-manager ${opts.dispatcherId}] ${lvl}`;
35
+ if (err !== undefined)
36
+ console.error(prefix, msg, err);
37
+ else
38
+ console.error(prefix, msg);
39
+ });
40
+ this.outboundRetries = opts.outboundRetries ?? 3;
41
+ this.outboundRetryDelayMs = opts.outboundRetryDelayMs ?? 1000;
42
+ this.emptyTurnPlaceholder =
43
+ opts.emptyTurnPlaceholder ?? '本轮没有文本回复。';
44
+ }
45
+ /**
46
+ * Notify the worker that new work may be available.
47
+ * Idempotent — multiple wakeups collapse into the next loop iteration.
48
+ */
49
+ notify() {
50
+ if (this.stopped)
51
+ return;
52
+ if (this.wakeup !== null) {
53
+ const w = this.wakeup;
54
+ this.wakeup = null;
55
+ w();
56
+ }
57
+ void this.drainLoop();
58
+ }
59
+ /** Drain queued rows until the queue is empty or we're stopped. */
60
+ async drainLoop() {
61
+ if (this.running)
62
+ return;
63
+ this.running = true;
64
+ try {
65
+ while (!this.stopped) {
66
+ const row = this.opts.inbound.takeNextQueued(this.opts.dispatcherId);
67
+ if (row === null) {
68
+ await this.waitForNotify();
69
+ if (this.stopped)
70
+ return;
71
+ continue;
72
+ }
73
+ await this.processInbound(row);
74
+ }
75
+ }
76
+ finally {
77
+ this.running = false;
78
+ }
79
+ }
80
+ waitForNotify() {
81
+ return new Promise((res) => {
82
+ this.wakeup = res;
83
+ });
84
+ }
85
+ async processInbound(row) {
86
+ const threadId = this.opts.getThreadId();
87
+ if (threadId === null) {
88
+ // Should not happen — dispatcher is "ready" only after thread is set.
89
+ this.log('error', `inbound row ${row.id} dequeued without thread_id`);
90
+ this.opts.inbound.markFailed(row.id, 'dispatcher has no thread_id');
91
+ return;
92
+ }
93
+ // Mark running first (before turn/start) so a crash mid-RPC still
94
+ // leaves a recoverable trace.
95
+ this.opts.inbound.markRunning(row.id, null);
96
+ let assistantText;
97
+ try {
98
+ const turn = await runTurn(this.opts.client, threadId, row.parsed_text, this.opts.turnCwd ?? null);
99
+ // Record the turn id for diagnostics — non-fatal if this column
100
+ // can't be updated (e.g. row was already advanced by another path).
101
+ this.opts.inbound.markRunning(row.id, turn.turnId);
102
+ assistantText =
103
+ extractAssistantText(turn) ?? this.emptyTurnPlaceholder;
104
+ }
105
+ catch (err) {
106
+ const msg = err instanceof Error ? err.message : String(err);
107
+ this.log('error', `turn execution failed for inbound ${row.id}: ${msg}`);
108
+ this.opts.inbound.markFailed(row.id, msg);
109
+ // Best-effort tell the user something went wrong.
110
+ try {
111
+ await this.opts.outbound.sendText(row.source_chat_id, `本次请求执行失败:${msg}`);
112
+ }
113
+ catch (sendErr) {
114
+ this.log('warn', `error notification also failed`, sendErr);
115
+ }
116
+ return;
117
+ }
118
+ this.opts.inbound.markAwaitingOutbound(row.id, assistantText);
119
+ await this.sendOutbound(row.id, row.source_chat_id, assistantText);
120
+ }
121
+ /** Send assistant text to feishu with bounded retry. */
122
+ async sendOutbound(inboundId, chatId, text) {
123
+ let lastError;
124
+ for (let attempt = 0; attempt <= this.outboundRetries; attempt++) {
125
+ try {
126
+ const ids = await this.opts.outbound.sendText(chatId, text);
127
+ this.opts.inbound.markCompleted(inboundId, ids);
128
+ return;
129
+ }
130
+ catch (err) {
131
+ lastError = err;
132
+ if (attempt < this.outboundRetries) {
133
+ await new Promise((r) => setTimeout(r, this.outboundRetryDelayMs));
134
+ }
135
+ }
136
+ }
137
+ const msg = lastError instanceof Error ? lastError.message : String(lastError);
138
+ this.log('error', `outbound send failed for inbound ${inboundId}: ${msg}`);
139
+ this.opts.inbound.markOutboundFailed(inboundId, msg);
140
+ }
141
+ /**
142
+ * Retry rows previously left in awaiting_outbound / outbound_failed
143
+ * (no Codex turn re-runs — assistant_text is already in the DB).
144
+ * Called once at dispatcher startup, after thread/resume succeeds.
145
+ */
146
+ async retryPendingOutbound() {
147
+ const pending = this.opts.inbound.listAwaitingOrFailedOutbound(this.opts.dispatcherId);
148
+ for (const row of pending) {
149
+ if (row.assistant_text === null) {
150
+ // Should not happen — awaiting_outbound implies assistant_text was set.
151
+ this.log('warn', `pending outbound ${row.id} has no assistant_text; marking failed`);
152
+ this.opts.inbound.markFailed(row.id, 'awaiting_outbound row missing assistant_text');
153
+ continue;
154
+ }
155
+ await this.sendOutbound(row.id, row.source_chat_id, row.assistant_text);
156
+ }
157
+ }
158
+ async stop() {
159
+ this.stopped = true;
160
+ if (this.wakeup !== null) {
161
+ const w = this.wakeup;
162
+ this.wakeup = null;
163
+ w();
164
+ }
165
+ }
166
+ }
167
+ //# sourceMappingURL=turn-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"turn-manager.js","sourceRoot":"","sources":["../../src/dispatcher/turn-manager.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,137 @@
1
+ /**
2
+ * The `FeishuBot` adapter — one per Dispatcher (D3: 1 Dispatcher = 1 Bot).
3
+ *
4
+ * Since issue #25 PR1 this is a thin adapter over `@excitedjs/feishu-transport`
5
+ * (the shared platform-I/O core): all Feishu SDK I/O — the inbound WebSocket,
6
+ * markdown→card render, content parse, the outbound message API — lives in the
7
+ * core, the single importer of `@larksuiteoapi/node-sdk`. This file only shapes
8
+ * the core's surface into the `FeishuBot` interface the server already wires:
9
+ * - `start(handler)` registers the `im.message.receive_v1` route, normalizes
10
+ * each raw event with the core's `parseInbound`, and forwards a
11
+ * `FeishuInboundEvent`. The route handler awaits `handler`, so the message is
12
+ * durably enqueued before the SDK acks (the deferred-ACK invariant).
13
+ * - `sendText(chatId, text)` delegates to the core's `send({ chatId }, text)`.
14
+ * The per-card size guard and the multi-card split both live in the core now.
15
+ * - `botOpenId` surfaces the core transport's `selfId`.
16
+ *
17
+ * Tests inject a `FakeFeishuBot` via `createFakeFeishuBot()` instead of opening
18
+ * a live connection.
19
+ */
20
+ import { createFeishuTransport, parseInbound, } from '@excitedjs/feishu-transport';
21
+ /** The Feishu event_type carrying inbound chat messages. */
22
+ const IM_MESSAGE_EVENT_TYPE = 'im.message.receive_v1';
23
+ export function createFeishuBot(opts) {
24
+ const transport = createFeishuTransport({
25
+ appId: opts.appId,
26
+ appSecret: opts.appSecret,
27
+ });
28
+ return {
29
+ get appId() {
30
+ return transport.appId;
31
+ },
32
+ get botOpenId() {
33
+ return transport.selfId;
34
+ },
35
+ async start(handler) {
36
+ // The core opens the WebSocket and awaits this route handler before the
37
+ // SDK acks; awaiting `handler` here keeps the enqueue durable-before-ACK.
38
+ // `start` rejects if the connection does not come up, so the server's
39
+ // try/catch can fail the dispatcher loudly rather than leave it dark.
40
+ await transport.start({
41
+ [IM_MESSAGE_EVENT_TYPE]: async (raw) => {
42
+ const event = normalizeInboundEvent(raw);
43
+ if (event === null)
44
+ return;
45
+ await handler(event);
46
+ },
47
+ });
48
+ },
49
+ async sendText(chatId, text) {
50
+ const { messageIds } = await transport.send({ chatId }, text);
51
+ return { messageIds };
52
+ },
53
+ close() {
54
+ return transport.close();
55
+ },
56
+ };
57
+ }
58
+ /**
59
+ * Reshape a raw `im.message.receive_v1` payload into a `FeishuInboundEvent`,
60
+ * using the core's `parseInbound` for the content→text flattening (incl. the
61
+ * `interactive`-card parse the old in-package copy had lost). Returns `null`
62
+ * for a payload missing the message_id or chat_id that make it routable.
63
+ */
64
+ function normalizeInboundEvent(raw) {
65
+ if (!raw || typeof raw !== 'object')
66
+ return null;
67
+ const root = raw;
68
+ const event = (root['event'] ?? root);
69
+ const message = (event['message'] ?? {});
70
+ const sender = (event['sender'] ?? {});
71
+ const senderId = sender['sender_id']?.['open_id'] ?? '';
72
+ const messageId = message['message_id'] ?? '';
73
+ const chatId = message['chat_id'] ?? '';
74
+ const chatType = message['chat_type'] ?? '';
75
+ const messageType = message['message_type'] ?? '';
76
+ const rawContent = message['content'] ?? '';
77
+ const mentions = message['mentions'] ?? [];
78
+ const createTime = message['create_time'] ?? '';
79
+ if (messageId === '' || chatId === '')
80
+ return null;
81
+ const parsed = parseInbound({
82
+ message_type: messageType,
83
+ content: rawContent,
84
+ mentions,
85
+ });
86
+ return {
87
+ messageId,
88
+ chatId,
89
+ chatType,
90
+ senderId,
91
+ messageType,
92
+ rawContent,
93
+ parsedText: parsed.text,
94
+ mentions,
95
+ createTime,
96
+ raw,
97
+ };
98
+ }
99
+ export function createFakeFeishuBot(appId = 'fake_bot') {
100
+ const sent = [];
101
+ let handler = null;
102
+ let nextMessageId = 1;
103
+ let sendError = null;
104
+ const openId = `ou_${appId}`;
105
+ return {
106
+ appId,
107
+ get botOpenId() {
108
+ return openId;
109
+ },
110
+ async start(h) {
111
+ handler = h;
112
+ },
113
+ async sendText(chatId, text) {
114
+ if (sendError !== null) {
115
+ throw sendError;
116
+ }
117
+ const id = `om_fake_${nextMessageId++}`;
118
+ sent.push({ chatId, text, messageIds: [id] });
119
+ return { messageIds: [id] };
120
+ },
121
+ async close() {
122
+ handler = null;
123
+ },
124
+ get sentMessages() {
125
+ return sent;
126
+ },
127
+ async inject(event) {
128
+ if (handler === null)
129
+ throw new Error('fake bot not started');
130
+ await handler(event);
131
+ },
132
+ setSendError(err) {
133
+ sendError = err;
134
+ },
135
+ };
136
+ }
137
+ //# sourceMappingURL=bot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bot.js","sourceRoot":"","sources":["../../src/feishu/bot.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Parsing inbound Feishu message content.
3
+ *
4
+ * Feishu delivers `message.content` as a JSON-encoded string whose shape
5
+ * depends on `message_type`. This module turns that into the plain text the
6
+ * channel forwards to Claude. Attachment message types (image, file) are
7
+ * summarized as a short text marker — the channel forwards text, not binaries.
8
+ */
9
+ /**
10
+ * Parse one inbound Feishu message into forwardable text. Never throws —
11
+ * malformed content falls back to a best-effort string so a weird message
12
+ * still reaches Claude.
13
+ */
14
+ export function parseInbound(message) {
15
+ const type = message.message_type ?? 'unknown';
16
+ let parsed;
17
+ try {
18
+ parsed = JSON.parse(message.content ?? '');
19
+ }
20
+ catch {
21
+ return { text: message.content ?? '(unparseable message)' };
22
+ }
23
+ const content = (parsed && typeof parsed === 'object' ? parsed : {});
24
+ switch (type) {
25
+ case 'text': {
26
+ const text = typeof content.text === 'string' ? content.text : '';
27
+ return { text: applyMentions(text, message.mentions) };
28
+ }
29
+ case 'post':
30
+ return { text: extractPostText(content) };
31
+ case 'image':
32
+ return { text: '(image)' };
33
+ case 'file': {
34
+ const fileName = typeof content.file_name === 'string' ? content.file_name : 'unknown';
35
+ return { text: `(file: ${fileName})` };
36
+ }
37
+ default:
38
+ return { text: `(${type} message)` };
39
+ }
40
+ }
41
+ /**
42
+ * Replace Feishu's `@_user_N` placeholders in text with the mentioned display
43
+ * names, so the forwarded message reads naturally.
44
+ */
45
+ export function applyMentions(text, mentions) {
46
+ if (!mentions)
47
+ return text;
48
+ let out = text;
49
+ for (const m of mentions) {
50
+ if (m.key && m.name) {
51
+ out = out.split(m.key).join(`@${m.name}`);
52
+ }
53
+ }
54
+ return out;
55
+ }
56
+ /**
57
+ * Flatten a Feishu rich-text "post" payload into plain text. A post is
58
+ * locale-wrapped (`{ zh_cn: { title, content } }`) and its body is an array of
59
+ * paragraphs, each an array of tagged inline elements.
60
+ */
61
+ export function extractPostText(content) {
62
+ const post = pickPostLocale(content);
63
+ const lines = [];
64
+ if (typeof post.title === 'string' && post.title.length > 0) {
65
+ lines.push(post.title);
66
+ }
67
+ const body = post.content;
68
+ if (Array.isArray(body)) {
69
+ for (const paragraph of body) {
70
+ if (!Array.isArray(paragraph))
71
+ continue;
72
+ lines.push(paragraph.map(renderPostElement).join(''));
73
+ }
74
+ }
75
+ return lines.join('\n');
76
+ }
77
+ /** Pick the first present locale block of a post, falling back to the raw object. */
78
+ function pickPostLocale(content) {
79
+ for (const locale of ['zh_cn', 'en_us', 'ja_jp']) {
80
+ const block = content[locale];
81
+ if (block && typeof block === 'object')
82
+ return block;
83
+ }
84
+ return content;
85
+ }
86
+ /** Render one inline post element to text. */
87
+ function renderPostElement(el) {
88
+ if (!el || typeof el !== 'object')
89
+ return '';
90
+ const e = el;
91
+ switch (e.tag) {
92
+ case 'text':
93
+ return typeof e.text === 'string' ? e.text : '';
94
+ case 'a':
95
+ return typeof e.text === 'string'
96
+ ? e.text
97
+ : typeof e.href === 'string'
98
+ ? e.href
99
+ : '';
100
+ case 'at':
101
+ return `@${typeof e.user_name === 'string' ? e.user_name : ''}`;
102
+ case 'img':
103
+ return '(image)';
104
+ default:
105
+ return '';
106
+ }
107
+ }
108
+ //# sourceMappingURL=content.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content.js","sourceRoot":"","sources":["../../src/feishu/content.ts"],"names":[],"mappings":"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"}