@modelzen/feishu-codex-bridge 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.
@@ -0,0 +1,47 @@
1
+ interface LogContext {
2
+ traceId?: string;
3
+ chatId?: string;
4
+ msgId?: string;
5
+ }
6
+ type LogFields = Record<string, unknown>;
7
+ declare const log: {
8
+ info(phase: string, event: string, fields?: LogFields): void;
9
+ warn(phase: string, event: string, fields?: LogFields): void;
10
+ fail(phase: string, err: unknown, fields?: LogFields): void;
11
+ };
12
+ /** Run `fn` inside a logging context; all `log.*` inside pick up the fields. */
13
+ declare function withTrace<T>(ctx: LogContext, fn: () => Promise<T>): Promise<T>;
14
+ declare function newTraceId(): string;
15
+
16
+ declare const paths: {
17
+ appDir: string;
18
+ cacheDir: string;
19
+ /** bot 注册表:保存的全部 bot + 当前选中的 appId */
20
+ botsFile: string;
21
+ /** app id / 租户 / 偏好(当前 bot;不含明文密钥) */
22
+ readonly configFile: string;
23
+ /** thread(话题) → codex thread_id + cwd + 会话级配置(当前 bot) */
24
+ readonly sessionsFile: string;
25
+ /** project(群) → cwd + 默认参数 注册表(当前 bot) */
26
+ readonly projectsFile: string;
27
+ /** 在跑的 start 进程注册中心(同 App 冲突检测;当前 bot) */
28
+ readonly processesFile: string;
29
+ secretsFile: string;
30
+ keystoreSaltFile: string;
31
+ npmCacheDir: string;
32
+ /** 空白项目默认落地目录 */
33
+ projectsRootDir: string;
34
+ larkCliDir: string;
35
+ larkCliBinDir: string;
36
+ codexCliDir: string;
37
+ codexCliBinDir: string;
38
+ /**
39
+ * Thin shell wrapper that lark-cli invokes to resolve secrets from the
40
+ * bridge's encrypted store. Written user-owned and non-symlinked so it
41
+ * passes lark-cli's AssertSecurePath audit.
42
+ */
43
+ secretsGetterScript: string;
44
+ mediaDir: string;
45
+ };
46
+
47
+ export { log, newTraceId, paths, withTrace };
package/dist/index.js ADDED
@@ -0,0 +1,224 @@
1
+ // src/core/logger.ts
2
+ import { AsyncLocalStorage } from "async_hooks";
3
+ import { createWriteStream, mkdirSync } from "fs";
4
+ import { open, readdir, rm, stat } from "fs/promises";
5
+ import { join as join2 } from "path";
6
+
7
+ // src/config/paths.ts
8
+ import { homedir } from "os";
9
+ import { join } from "path";
10
+ var appDir = join(homedir(), ".feishu-codex-bridge");
11
+ var larkCliDir = join(appDir, "lark-cli");
12
+ var codexCliDir = join(appDir, "codex-cli");
13
+ var currentBotDir = appDir;
14
+ var paths = {
15
+ appDir,
16
+ cacheDir: appDir,
17
+ /** bot 注册表:保存的全部 bot + 当前选中的 appId */
18
+ botsFile: join(appDir, "bots.json"),
19
+ /** app id / 租户 / 偏好(当前 bot;不含明文密钥) */
20
+ get configFile() {
21
+ return join(currentBotDir, "config.json");
22
+ },
23
+ /** thread(话题) → codex thread_id + cwd + 会话级配置(当前 bot) */
24
+ get sessionsFile() {
25
+ return join(currentBotDir, "sessions.json");
26
+ },
27
+ /** project(群) → cwd + 默认参数 注册表(当前 bot) */
28
+ get projectsFile() {
29
+ return join(currentBotDir, "projects.json");
30
+ },
31
+ /** 在跑的 start 进程注册中心(同 App 冲突检测;当前 bot) */
32
+ get processesFile() {
33
+ return join(currentBotDir, "processes.json");
34
+ },
35
+ secretsFile: join(appDir, "secrets.enc"),
36
+ keystoreSaltFile: join(appDir, ".keystore.salt"),
37
+ npmCacheDir: join(appDir, "npm-cache"),
38
+ /** 空白项目默认落地目录 */
39
+ projectsRootDir: join(appDir, "projects"),
40
+ larkCliDir,
41
+ larkCliBinDir: join(larkCliDir, "node_modules", ".bin"),
42
+ codexCliDir,
43
+ codexCliBinDir: join(codexCliDir, "node_modules", ".bin"),
44
+ /**
45
+ * Thin shell wrapper that lark-cli invokes to resolve secrets from the
46
+ * bridge's encrypted store. Written user-owned and non-symlinked so it
47
+ * passes lark-cli's AssertSecurePath audit.
48
+ */
49
+ secretsGetterScript: join(appDir, "secrets-getter"),
50
+ mediaDir: join(appDir, "media")
51
+ };
52
+
53
+ // src/core/logger.ts
54
+ var LOG_RETENTION_DAYS = Math.max(
55
+ 1,
56
+ Number(process.env.FEISHU_CODEX_LOG_DAYS ?? 7) || 7
57
+ );
58
+ var STDOUT_INFO_ALLOWLIST = /* @__PURE__ */ new Set([
59
+ "ws.connected",
60
+ "ws.reconnecting",
61
+ "ws.reconnected",
62
+ "intake.enter",
63
+ "intake.recv",
64
+ "intake.reject",
65
+ "card.final",
66
+ "card.config",
67
+ "card.action",
68
+ "card.launch",
69
+ "agent.spawn",
70
+ "agent.exit"
71
+ ]);
72
+ var als = new AsyncLocalStorage();
73
+ var stream = null;
74
+ var currentDate = "";
75
+ function todayKey() {
76
+ return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
77
+ }
78
+ function logsDir() {
79
+ return join2(paths.appDir, "logs");
80
+ }
81
+ function getStream() {
82
+ const today = todayKey();
83
+ if (stream && currentDate === today) return stream;
84
+ if (stream) {
85
+ try {
86
+ stream.end();
87
+ } catch {
88
+ }
89
+ }
90
+ try {
91
+ mkdirSync(logsDir(), { recursive: true });
92
+ stream = createWriteStream(join2(logsDir(), `${today}.log`), { flags: "a" });
93
+ currentDate = today;
94
+ return stream;
95
+ } catch {
96
+ return null;
97
+ }
98
+ }
99
+ var RESERVED_KEYS = /* @__PURE__ */ new Set([
100
+ "ts",
101
+ "level",
102
+ "phase",
103
+ "event",
104
+ "traceId",
105
+ "chatId",
106
+ "msgId"
107
+ ]);
108
+ function emit(level, phase, event, fields = {}) {
109
+ const ctx = als.getStore() ?? {};
110
+ const entry = {
111
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
112
+ level,
113
+ phase,
114
+ event,
115
+ ...ctx
116
+ };
117
+ for (const [k, v] of Object.entries(fields)) {
118
+ if (RESERVED_KEYS.has(k)) {
119
+ entry[`_${k}`] = v;
120
+ } else {
121
+ entry[k] = v;
122
+ }
123
+ }
124
+ const s = getStream();
125
+ if (s) {
126
+ try {
127
+ s.write(`${JSON.stringify(entry)}
128
+ `);
129
+ } catch {
130
+ }
131
+ }
132
+ const showOnStdout = level !== "info" || STDOUT_INFO_ALLOWLIST.has(`${phase}.${event}`);
133
+ if (!showOnStdout) return;
134
+ const fn = level === "error" ? console.error : level === "warn" ? console.warn : console.log;
135
+ fn(formatStdout(level, phase, event, ctx, fields));
136
+ }
137
+ function formatStdout(level, phase, event, ctx, fields) {
138
+ if (phase === "ws") {
139
+ if (event === "connected") {
140
+ const bot = fields.bot ?? "-";
141
+ const appId = fields.appId ? ` (${fields.appId})` : "";
142
+ return `\u2713 \u5DF2\u8FDE\u63A5 bot: ${bot}${appId}`;
143
+ }
144
+ if (event === "reconnecting") return "\u21BB \u6B63\u5728\u91CD\u8FDE\u2026";
145
+ if (event === "reconnected") return "\u2713 \u5DF2\u91CD\u8FDE";
146
+ if (event === "fail") return `\u2717 WS \u9519\u8BEF: ${fields.err ?? ""}`;
147
+ }
148
+ if (phase === "intake" && event === "enter") {
149
+ const c = ctx.chatId ? ctx.chatId.slice(-6) : "-";
150
+ const sender = fields.sender ?? "-";
151
+ const preview = fields.preview ?? "";
152
+ return `\u25B8 ${fields.chatType ?? "?"}/${c} ${sender}: ${preview}`;
153
+ }
154
+ if (phase === "card" && event === "final") {
155
+ const c = ctx.chatId ? ctx.chatId.slice(-6) : "-";
156
+ const t = fields.terminal;
157
+ const mark = t === "done" ? "\u2713" : t === "interrupted" ? "\u23F9" : "\u2717";
158
+ return ` ${mark} ${c} ${t}`;
159
+ }
160
+ const ctxBits = [];
161
+ if (ctx.traceId) ctxBits.push(`t=${ctx.traceId}`);
162
+ if (ctx.chatId) ctxBits.push(`c=${ctx.chatId.slice(-6)}`);
163
+ const ctxStr = ctxBits.length > 0 ? ` ${ctxBits.join(" ")}` : "";
164
+ const summary = formatFields(fields);
165
+ const tag = level === "error" ? "\u2717" : level === "warn" ? "\u26A0" : "\xB7";
166
+ return `${tag} [${phase}.${event}]${ctxStr}${summary ? ` ${summary}` : ""}`;
167
+ }
168
+ function formatFields(fields) {
169
+ const keys = Object.keys(fields);
170
+ if (keys.length === 0) return "";
171
+ const parts = [];
172
+ for (const k of keys) {
173
+ const v = fields[k];
174
+ if (v === void 0 || v === null) continue;
175
+ if (k === "stack") continue;
176
+ if (typeof v === "string") {
177
+ parts.push(`${k}=${v.length > 80 ? `${v.slice(0, 80)}\u2026` : v}`);
178
+ } else if (typeof v === "number" || typeof v === "boolean") {
179
+ parts.push(`${k}=${v}`);
180
+ } else {
181
+ try {
182
+ const str = JSON.stringify(v);
183
+ parts.push(`${k}=${str.length > 80 ? `${str.slice(0, 80)}\u2026` : str}`);
184
+ } catch {
185
+ parts.push(`${k}=?`);
186
+ }
187
+ }
188
+ }
189
+ return parts.join(" ");
190
+ }
191
+ var log = {
192
+ info(phase, event, fields) {
193
+ emit("info", phase, event, fields);
194
+ },
195
+ warn(phase, event, fields) {
196
+ emit("warn", phase, event, fields);
197
+ },
198
+ fail(phase, err, fields) {
199
+ const message = err instanceof Error ? err.message : String(err);
200
+ const stack = err instanceof Error ? err.stack : void 0;
201
+ const apiData = err?.response?.data;
202
+ const apiStatus = err?.response?.status;
203
+ emit("error", phase, "fail", {
204
+ ...fields,
205
+ err: message,
206
+ apiStatus,
207
+ apiData,
208
+ stack
209
+ });
210
+ }
211
+ };
212
+ function withTrace(ctx, fn) {
213
+ const traceId = ctx.traceId ?? newTraceId();
214
+ return als.run({ ...ctx, traceId }, fn);
215
+ }
216
+ function newTraceId() {
217
+ return Math.random().toString(36).slice(2, 10);
218
+ }
219
+ export {
220
+ log,
221
+ newTraceId,
222
+ paths,
223
+ withTrace
224
+ };
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@modelzen/feishu-codex-bridge",
3
+ "version": "0.1.0",
4
+ "description": "Bridge Feishu/Lark messenger with local Codex via app-server (project=group, thread=session)",
5
+ "type": "module",
6
+ "bin": {
7
+ "feishu-codex-bridge": "bin/feishu-codex-bridge.mjs"
8
+ },
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "bin",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsup",
23
+ "dev": "tsup --watch",
24
+ "typecheck": "tsc --noEmit",
25
+ "test": "vitest run",
26
+ "start": "node bin/feishu-codex-bridge.mjs run",
27
+ "prepare": "npm run build"
28
+ },
29
+ "dependencies": {
30
+ "@larksuiteoapi/node-sdk": "^1.65.0",
31
+ "commander": "^12.1.0",
32
+ "qrcode-terminal": "^0.12.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^22.10.0",
36
+ "@types/qrcode-terminal": "^0.12.2",
37
+ "tsup": "^8.3.5",
38
+ "typescript": "^5.6.3",
39
+ "vitest": "^2.1.8"
40
+ },
41
+ "engines": {
42
+ "node": ">=20.0.0"
43
+ },
44
+ "keywords": [
45
+ "feishu",
46
+ "lark",
47
+ "codex",
48
+ "app-server",
49
+ "cli",
50
+ "bridge",
51
+ "bot"
52
+ ],
53
+ "author": "modelzen",
54
+ "license": "MIT",
55
+ "homepage": "https://github.com/modelzen/feishu-codex-bridge#readme",
56
+ "repository": {
57
+ "type": "git",
58
+ "url": "git+https://github.com/modelzen/feishu-codex-bridge.git"
59
+ },
60
+ "bugs": {
61
+ "url": "https://github.com/modelzen/feishu-codex-bridge/issues"
62
+ },
63
+ "publishConfig": {
64
+ "access": "public"
65
+ }
66
+ }