@openai-lite/codex-feishu 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +164 -0
- package/bin/codex-feishu.js +8 -0
- package/docs/ARCHITECTURE.md +118 -0
- package/docs/FEISHU_SETUP.zh-CN.md +46 -0
- package/docs/README.zh-CN.md +130 -0
- package/docs/assets/feishu-preview-image-reply.png +0 -0
- package/docs/assets/feishu-preview-image-reply1.jpg +0 -0
- package/docs/assets/feishu-preview-session.png +0 -0
- package/package.json +23 -0
- package/src/cli.js +158 -0
- package/src/commands/daemon.js +6037 -0
- package/src/commands/doctor.js +126 -0
- package/src/commands/inbound.js +27 -0
- package/src/commands/init.js +224 -0
- package/src/commands/mcp.js +295 -0
- package/src/commands/qrcode.js +163 -0
- package/src/lib/app_server_client.js +831 -0
- package/src/lib/daemon_control.js +190 -0
- package/src/lib/feishu_bridge.js +461 -0
- package/src/lib/fs_utils.js +41 -0
- package/src/lib/paths.js +59 -0
- package/src/lib/state_store.js +146 -0
- package/src/lib/uds_rpc.js +195 -0
package/src/lib/paths.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const WINDOWS_DEFAULT_RPC_ENDPOINT = "tcp://127.0.0.1:9765";
|
|
5
|
+
|
|
6
|
+
export function getCodexHome() {
|
|
7
|
+
return process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function getBridgeHome() {
|
|
11
|
+
return process.env.CODEX_FEISHU_HOME || path.join(os.homedir(), ".codex-feishu");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getCodexConfigPath() {
|
|
15
|
+
return path.join(getCodexHome(), "config.toml");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getPromptsDir() {
|
|
19
|
+
return path.join(getCodexHome(), "prompts");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getBridgeConfigPath() {
|
|
23
|
+
return path.join(getBridgeHome(), "config.json");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getBridgeStatePath() {
|
|
27
|
+
return path.join(getBridgeHome(), "state.json");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getRunDir() {
|
|
31
|
+
return path.join(getBridgeHome(), "run");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getBridgeSocketPath() {
|
|
35
|
+
return path.join(getRunDir(), "bridge.sock");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getDefaultBridgeRpcEndpoint() {
|
|
39
|
+
if (process.platform === "win32") {
|
|
40
|
+
return WINDOWS_DEFAULT_RPC_ENDPOINT;
|
|
41
|
+
}
|
|
42
|
+
return getBridgeSocketPath();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getBridgeRpcEndpoint() {
|
|
46
|
+
const fromEnv = process.env.CODEX_FEISHU_RPC_ENDPOINT;
|
|
47
|
+
if (typeof fromEnv === "string" && fromEnv.trim().length > 0) {
|
|
48
|
+
return fromEnv.trim();
|
|
49
|
+
}
|
|
50
|
+
return getDefaultBridgeRpcEndpoint();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function getDaemonPidPath() {
|
|
54
|
+
return path.join(getRunDir(), "daemon.pid");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function getDaemonLogPath() {
|
|
58
|
+
return path.join(getRunDir(), "daemon.log");
|
|
59
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { readTextIfExists, writeJson } from "./fs_utils.js";
|
|
4
|
+
|
|
5
|
+
const STATE_VERSION = 1;
|
|
6
|
+
const MAX_RECENT_EVENTS = 500;
|
|
7
|
+
|
|
8
|
+
function nowTs() {
|
|
9
|
+
return Date.now();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function defaultState() {
|
|
13
|
+
const ts = nowTs();
|
|
14
|
+
return {
|
|
15
|
+
version: STATE_VERSION,
|
|
16
|
+
created_at: ts,
|
|
17
|
+
updated_at: ts,
|
|
18
|
+
active_thread_id: null,
|
|
19
|
+
pending_requests: {},
|
|
20
|
+
bindings: {},
|
|
21
|
+
pending_bind_codes: {},
|
|
22
|
+
last_qrcode_cwd: null,
|
|
23
|
+
thread_titles: {},
|
|
24
|
+
thread_buffers: {},
|
|
25
|
+
recent_events: [],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeObject(value) {
|
|
30
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeArray(value) {
|
|
34
|
+
return Array.isArray(value) ? value : [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeState(raw) {
|
|
38
|
+
const base = defaultState();
|
|
39
|
+
if (!raw || typeof raw !== "object") {
|
|
40
|
+
return base;
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
...base,
|
|
44
|
+
...raw,
|
|
45
|
+
version: STATE_VERSION,
|
|
46
|
+
pending_requests: normalizeObject(raw.pending_requests),
|
|
47
|
+
bindings: normalizeObject(raw.bindings),
|
|
48
|
+
pending_bind_codes: normalizeObject(raw.pending_bind_codes),
|
|
49
|
+
last_qrcode_cwd: typeof raw.last_qrcode_cwd === "string" ? raw.last_qrcode_cwd : null,
|
|
50
|
+
thread_titles: normalizeObject(raw.thread_titles),
|
|
51
|
+
thread_buffers: normalizeObject(raw.thread_buffers),
|
|
52
|
+
recent_events: normalizeArray(raw.recent_events).slice(-MAX_RECENT_EVENTS),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function toPersistedState(state) {
|
|
57
|
+
return {
|
|
58
|
+
version: STATE_VERSION,
|
|
59
|
+
created_at: state.created_at ?? nowTs(),
|
|
60
|
+
updated_at: state.updated_at ?? nowTs(),
|
|
61
|
+
active_thread_id: state.active_thread_id ?? null,
|
|
62
|
+
bindings: normalizeObject(state.bindings),
|
|
63
|
+
pending_bind_codes: normalizeObject(state.pending_bind_codes),
|
|
64
|
+
last_qrcode_cwd: typeof state.last_qrcode_cwd === "string" ? state.last_qrcode_cwd : null,
|
|
65
|
+
thread_titles: normalizeObject(state.thread_titles),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export class StateStore {
|
|
70
|
+
constructor(statePath) {
|
|
71
|
+
this.statePath = statePath;
|
|
72
|
+
this.state = defaultState();
|
|
73
|
+
this.queue = Promise.resolve();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async load() {
|
|
77
|
+
const text = await readTextIfExists(this.statePath);
|
|
78
|
+
if (!text) {
|
|
79
|
+
this.state = defaultState();
|
|
80
|
+
return this.state;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const raw = JSON.parse(text);
|
|
84
|
+
this.state = normalizeState(raw);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
const backupPath = `${this.statePath}.corrupt-${Date.now()}`;
|
|
87
|
+
try {
|
|
88
|
+
await fs.rename(this.statePath, backupPath);
|
|
89
|
+
} catch {
|
|
90
|
+
// noop
|
|
91
|
+
}
|
|
92
|
+
this.state = defaultState();
|
|
93
|
+
pushRecentEvent(this.state, {
|
|
94
|
+
source: "daemon",
|
|
95
|
+
type: "state_recovered_from_corrupt_json",
|
|
96
|
+
backup_path: backupPath,
|
|
97
|
+
error: err?.message ?? String(err),
|
|
98
|
+
});
|
|
99
|
+
await this.save();
|
|
100
|
+
}
|
|
101
|
+
return this.state;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
snapshot() {
|
|
105
|
+
return this.state;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async save() {
|
|
109
|
+
this.state.updated_at = nowTs();
|
|
110
|
+
await writeJson(this.statePath, toPersistedState(this.state));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async mutate(mutator) {
|
|
114
|
+
const run = async () => {
|
|
115
|
+
const maybeNext = await mutator(this.state);
|
|
116
|
+
if (maybeNext) {
|
|
117
|
+
this.state = normalizeState(maybeNext);
|
|
118
|
+
} else {
|
|
119
|
+
this.state = normalizeState(this.state);
|
|
120
|
+
}
|
|
121
|
+
await this.save();
|
|
122
|
+
return this.state;
|
|
123
|
+
};
|
|
124
|
+
const next = this.queue.then(run, run);
|
|
125
|
+
this.queue = next.then(
|
|
126
|
+
() => undefined,
|
|
127
|
+
() => undefined,
|
|
128
|
+
);
|
|
129
|
+
return next;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function createBindCode() {
|
|
134
|
+
return `CF-${crypto.randomBytes(4).toString("hex").toUpperCase()}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function pushRecentEvent(state, event) {
|
|
138
|
+
const next = {
|
|
139
|
+
ts: nowTs(),
|
|
140
|
+
...event,
|
|
141
|
+
};
|
|
142
|
+
const recent = normalizeArray(state.recent_events);
|
|
143
|
+
recent.push(next);
|
|
144
|
+
state.recent_events = recent.slice(-MAX_RECENT_EVENTS);
|
|
145
|
+
return next;
|
|
146
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
import readline from "node:readline";
|
|
3
|
+
|
|
4
|
+
function jsonrpcResult(id, result) {
|
|
5
|
+
return { jsonrpc: "2.0", id, result };
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function jsonrpcError(id, code, message, data) {
|
|
9
|
+
const payload = {
|
|
10
|
+
jsonrpc: "2.0",
|
|
11
|
+
id,
|
|
12
|
+
error: {
|
|
13
|
+
code,
|
|
14
|
+
message,
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
if (data !== undefined) {
|
|
18
|
+
payload.error.data = data;
|
|
19
|
+
}
|
|
20
|
+
return payload;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function writeJsonLine(socket, value) {
|
|
24
|
+
socket.write(`${JSON.stringify(value)}\n`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function parseRpcEndpoint(endpoint) {
|
|
28
|
+
if (typeof endpoint !== "string" || endpoint.length === 0) {
|
|
29
|
+
throw new Error("rpc endpoint is required");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (endpoint.startsWith("tcp://")) {
|
|
33
|
+
const raw = endpoint.slice("tcp://".length);
|
|
34
|
+
const [host, portText] = raw.split(":");
|
|
35
|
+
const port = Number.parseInt(portText, 10);
|
|
36
|
+
if (!host || !Number.isFinite(port) || port <= 0) {
|
|
37
|
+
throw new Error(`invalid tcp rpc endpoint: ${endpoint}`);
|
|
38
|
+
}
|
|
39
|
+
return { kind: "tcp", host, port };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { kind: "unix", path: endpoint };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function createJsonRpcServer({ endpoint, onRequest, onNotification }) {
|
|
46
|
+
const parsedEndpoint = parseRpcEndpoint(endpoint);
|
|
47
|
+
const server = net.createServer((socket) => {
|
|
48
|
+
const rl = readline.createInterface({ input: socket, crlfDelay: Infinity });
|
|
49
|
+
|
|
50
|
+
rl.on("line", async (line) => {
|
|
51
|
+
const trimmed = line.trim();
|
|
52
|
+
if (!trimmed) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let msg;
|
|
57
|
+
try {
|
|
58
|
+
msg = JSON.parse(trimmed);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
writeJsonLine(socket, jsonrpcError(null, -32700, "Parse error", String(err)));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const isRequest = msg && typeof msg === "object" && "method" in msg && "id" in msg;
|
|
65
|
+
const isNotification = msg && typeof msg === "object" && "method" in msg && !("id" in msg);
|
|
66
|
+
if (!isRequest && !isNotification) {
|
|
67
|
+
writeJsonLine(socket, jsonrpcError(msg?.id ?? null, -32600, "Invalid Request"));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (isNotification) {
|
|
72
|
+
if (onNotification) {
|
|
73
|
+
try {
|
|
74
|
+
await onNotification(msg.method, msg.params ?? null, { socket });
|
|
75
|
+
} catch {
|
|
76
|
+
// Notifications do not carry response.
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const result = await onRequest(msg.method, msg.params ?? null, {
|
|
84
|
+
id: msg.id,
|
|
85
|
+
socket,
|
|
86
|
+
});
|
|
87
|
+
writeJsonLine(socket, jsonrpcResult(msg.id, result ?? null));
|
|
88
|
+
} catch (err) {
|
|
89
|
+
writeJsonLine(socket, jsonrpcError(msg.id, -32000, err?.message ?? "Internal error"));
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
await new Promise((resolve, reject) => {
|
|
95
|
+
const onError = (err) => {
|
|
96
|
+
server.off("listening", onListening);
|
|
97
|
+
reject(err);
|
|
98
|
+
};
|
|
99
|
+
const onListening = () => {
|
|
100
|
+
server.off("error", onError);
|
|
101
|
+
resolve();
|
|
102
|
+
};
|
|
103
|
+
server.once("error", onError);
|
|
104
|
+
server.once("listening", onListening);
|
|
105
|
+
|
|
106
|
+
if (parsedEndpoint.kind === "unix") {
|
|
107
|
+
server.listen(parsedEndpoint.path);
|
|
108
|
+
} else {
|
|
109
|
+
server.listen(parsedEndpoint.port, parsedEndpoint.host);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return server;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function callJsonRpc(endpoint, method, params = {}, options = {}) {
|
|
117
|
+
const parsedEndpoint = parseRpcEndpoint(endpoint);
|
|
118
|
+
const timeoutMs = options.timeoutMs ?? 5000;
|
|
119
|
+
const requestId = `${Date.now()}-${Math.floor(Math.random() * 1e6)}`;
|
|
120
|
+
|
|
121
|
+
return new Promise((resolve, reject) => {
|
|
122
|
+
const socket =
|
|
123
|
+
parsedEndpoint.kind === "unix"
|
|
124
|
+
? net.createConnection(parsedEndpoint.path)
|
|
125
|
+
: net.createConnection(parsedEndpoint.port, parsedEndpoint.host);
|
|
126
|
+
const rl = readline.createInterface({ input: socket, crlfDelay: Infinity });
|
|
127
|
+
|
|
128
|
+
let settled = false;
|
|
129
|
+
const timeout = setTimeout(() => {
|
|
130
|
+
if (settled) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
settled = true;
|
|
134
|
+
socket.destroy();
|
|
135
|
+
reject(new Error(`rpc timeout after ${timeoutMs}ms`));
|
|
136
|
+
}, timeoutMs);
|
|
137
|
+
|
|
138
|
+
function done(fn, value) {
|
|
139
|
+
if (settled) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
settled = true;
|
|
143
|
+
clearTimeout(timeout);
|
|
144
|
+
rl.close();
|
|
145
|
+
socket.destroy();
|
|
146
|
+
fn(value);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
socket.on("connect", () => {
|
|
150
|
+
writeJsonLine(socket, {
|
|
151
|
+
jsonrpc: "2.0",
|
|
152
|
+
id: requestId,
|
|
153
|
+
method,
|
|
154
|
+
params,
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
socket.on("error", (err) => {
|
|
159
|
+
done(reject, err);
|
|
160
|
+
});
|
|
161
|
+
socket.on("close", () => {
|
|
162
|
+
if (!settled) {
|
|
163
|
+
done(reject, new Error("rpc connection closed"));
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
rl.on("error", (err) => {
|
|
168
|
+
done(reject, err);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
rl.on("line", (line) => {
|
|
172
|
+
const trimmed = line.trim();
|
|
173
|
+
if (!trimmed) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
let msg;
|
|
177
|
+
try {
|
|
178
|
+
msg = JSON.parse(trimmed);
|
|
179
|
+
} catch {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (msg?.id !== requestId) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (msg.error) {
|
|
186
|
+
const err = new Error(msg.error.message || "rpc error");
|
|
187
|
+
err.code = msg.error.code;
|
|
188
|
+
err.data = msg.error.data;
|
|
189
|
+
done(reject, err);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
done(resolve, msg.result);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
}
|