feishu-codex-console 1.0.0-beta.6

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 (113) hide show
  1. package/.env.example +101 -0
  2. package/.feishu-codex-policy.example.json +11 -0
  3. package/.feishu-codex-runbooks.example.json +36 -0
  4. package/CHANGELOG.md +129 -0
  5. package/CODE_OF_CONDUCT.md +7 -0
  6. package/CONTRIBUTING.md +52 -0
  7. package/LICENSE +21 -0
  8. package/README.en.md +81 -0
  9. package/README.md +398 -0
  10. package/ROADMAP.md +40 -0
  11. package/SECURITY.md +53 -0
  12. package/dist/account-quota-card.js +233 -0
  13. package/dist/account-quota.js +125 -0
  14. package/dist/app-server-client.js +281 -0
  15. package/dist/card-session.js +166 -0
  16. package/dist/codex-events.js +1 -0
  17. package/dist/codex-runner.js +875 -0
  18. package/dist/config.js +198 -0
  19. package/dist/confirmation-card.js +135 -0
  20. package/dist/control-card.js +345 -0
  21. package/dist/conversation-turn-session.js +209 -0
  22. package/dist/data-maintenance.js +71 -0
  23. package/dist/device-card.js +460 -0
  24. package/dist/device-health.js +94 -0
  25. package/dist/diagnostics.js +253 -0
  26. package/dist/doctor.js +250 -0
  27. package/dist/fallback-card-session.js +37 -0
  28. package/dist/health-file.js +75 -0
  29. package/dist/index.js +4330 -0
  30. package/dist/lark-cli.js +558 -0
  31. package/dist/lark-retry.js +34 -0
  32. package/dist/maintenance.js +140 -0
  33. package/dist/model-capabilities.js +31 -0
  34. package/dist/onboarding-card.js +312 -0
  35. package/dist/permission-lease.js +22 -0
  36. package/dist/policy.js +506 -0
  37. package/dist/progress.js +267 -0
  38. package/dist/project-card.js +303 -0
  39. package/dist/project-overview-card.js +182 -0
  40. package/dist/project-overview.js +278 -0
  41. package/dist/project-policy.js +160 -0
  42. package/dist/project-registry.js +259 -0
  43. package/dist/project-status.js +45 -0
  44. package/dist/project-workspace.js +55 -0
  45. package/dist/quota-card.js +94 -0
  46. package/dist/recovery-policy.js +26 -0
  47. package/dist/redaction.js +67 -0
  48. package/dist/remote-ready.js +112 -0
  49. package/dist/response-card.js +139 -0
  50. package/dist/result-card.js +166 -0
  51. package/dist/review-card.js +452 -0
  52. package/dist/runbook-card.js +272 -0
  53. package/dist/runbooks.js +191 -0
  54. package/dist/runtime-card.js +337 -0
  55. package/dist/session-card.js +128 -0
  56. package/dist/session-naming.js +14 -0
  57. package/dist/smoke.js +28 -0
  58. package/dist/state-backup.js +302 -0
  59. package/dist/state-store.js +874 -0
  60. package/dist/task-card.js +640 -0
  61. package/dist/task-center-card.js +176 -0
  62. package/dist/task-failure.js +43 -0
  63. package/dist/task-intent.js +76 -0
  64. package/dist/task-queue.js +187 -0
  65. package/dist/task-reconciliation.js +80 -0
  66. package/dist/task-review.js +497 -0
  67. package/dist/team-card.js +275 -0
  68. package/dist/team-directory.js +54 -0
  69. package/dist/team-policy.js +93 -0
  70. package/dist/types.js +1 -0
  71. package/dist/version.js +9 -0
  72. package/dist/workspace-session.js +64 -0
  73. package/docs/ARCHITECTURE.md +54 -0
  74. package/docs/COMPATIBILITY.md +55 -0
  75. package/docs/CONFIGURATION.md +88 -0
  76. package/docs/DEMO.md +45 -0
  77. package/docs/GOOD_FIRST_ISSUES.md +23 -0
  78. package/docs/INSTALLATION.md +207 -0
  79. package/docs/OPEN_SOURCE_PRODUCT_PLAN.md +113 -0
  80. package/docs/PRODUCT_REQUIREMENTS_MAP.md +591 -0
  81. package/docs/RELEASE_CHECKLIST.md +65 -0
  82. package/docs/TEAM_DEPLOYMENT.md +35 -0
  83. package/docs/TROUBLESHOOTING.md +130 -0
  84. package/docs/V4_WORKSPACE_SESSION_FLOW.md +232 -0
  85. package/docs/requirements/D10_MAINTENANCE_AND_ECOSYSTEM.md +103 -0
  86. package/docs/requirements/D1_INSTALLATION_AND_FIRST_CONNECTION.md +479 -0
  87. package/docs/requirements/D2_DEVICE_AND_CONNECTIVITY.md +54 -0
  88. package/docs/requirements/D3_PROJECTS_AND_SESSIONS.md +107 -0
  89. package/docs/requirements/D4_REMOTE_TASK_EXECUTION.md +102 -0
  90. package/docs/requirements/D5_CODEX_NATIVE_INTERACTIONS.md +99 -0
  91. package/docs/requirements/D6_RESULTS_AND_CODE_REVIEW.md +100 -0
  92. package/docs/requirements/D7_SECURITY_GOVERNANCE.md +106 -0
  93. package/docs/requirements/D8_RELIABILITY_AND_RECOVERY.md +182 -0
  94. package/docs/requirements/D9_TEAM_COLLABORATION.md +129 -0
  95. package/package.json +76 -0
  96. package/scripts/capability-probe.mjs +113 -0
  97. package/scripts/cli.mjs +919 -0
  98. package/scripts/config-file.mjs +137 -0
  99. package/scripts/discovery-lib.mjs +78 -0
  100. package/scripts/install-card.mjs +37 -0
  101. package/scripts/install-detection.mjs +126 -0
  102. package/scripts/install-state.mjs +107 -0
  103. package/scripts/launchd.mjs +161 -0
  104. package/scripts/migrate-legacy.mjs +97 -0
  105. package/scripts/package-smoke.mjs +163 -0
  106. package/scripts/release-dist-tag.mjs +7 -0
  107. package/scripts/runbook-template.mjs +36 -0
  108. package/scripts/service-health.mjs +110 -0
  109. package/scripts/service.mjs +24 -0
  110. package/scripts/setup-lib.mjs +118 -0
  111. package/scripts/systemd.mjs +96 -0
  112. package/scripts/upgrade-lib.mjs +99 -0
  113. package/scripts/verify-release.mjs +37 -0
@@ -0,0 +1,919 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawnSync } from "node:child_process";
4
+ import { chmod, mkdir, readFile, stat } from "node:fs/promises";
5
+ import { createInterface } from "node:readline/promises";
6
+ import { stdin as input, stdout as output } from "node:process";
7
+ import path from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+
10
+ import {
11
+ SETUP_PRESETS,
12
+ buildEnvFile,
13
+ defaultSetupPaths,
14
+ normalizeInstanceId,
15
+ splitCsv,
16
+ validateChatIds,
17
+ validateOpenIds,
18
+ } from "./setup-lib.mjs";
19
+ import { discoverFeishuIdentity } from "./discovery-lib.mjs";
20
+ import { detectExistingInstallation } from "./install-detection.mjs";
21
+ import { probeFeishuCapabilities } from "./capability-probe.mjs";
22
+ import { readEnvFile, writeManagedConfig } from "./config-file.mjs";
23
+ import { sendInstallationCard } from "./install-card.mjs";
24
+ import { waitForServiceHealth } from "./service-health.mjs";
25
+ import { migrateLegacyInstallation } from "./migrate-legacy.mjs";
26
+ import {
27
+ loadInstallState,
28
+ recordInstallError,
29
+ recordInstallStep,
30
+ } from "./install-state.mjs";
31
+ import { assessUpgradeReadiness, parseBackupId } from "./upgrade-lib.mjs";
32
+ import { initializeRunbookTemplate } from "./runbook-template.mjs";
33
+
34
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
35
+ const args = process.argv.slice(2);
36
+ const command = args[0] && !args[0].startsWith("-") ? args.shift() : "help";
37
+ const flags = parseFlags(args);
38
+
39
+ try {
40
+ if (command === "init" || command === "onboard") await initialize(flags);
41
+ else if (command === "migrate") await migrateLegacy(flags);
42
+ else if (command === "discover") await discover(flags);
43
+ else if (command === "doctor") runProjectCommand("doctor", flags);
44
+ else if (command === "backup") runDataCommand("backup", flags);
45
+ else if (command === "backups") runDataCommand("list", flags);
46
+ else if (command === "rollback") runDataCommand("rollback", flags);
47
+ else if (command === "support-bundle") await generateSupportBundle(flags);
48
+ else if (command === "version" || command === "compatibility") await printVersionInfo(flags);
49
+ else if (command === "init-runbooks") await initializeRunbooks(flags);
50
+ else if (command === "upgrade") await upgradeService(flags);
51
+ else if (command === "start" || command === "install") await installService(flags);
52
+ else if (["status", "stop", "restart"].includes(command)) runServiceCommand(command, flags);
53
+ else if (command === "uninstall") runServiceCommand("uninstall", flags);
54
+ else if (command === "help" || command === "--help" || command === "-h") printHelp();
55
+ else throw new Error(`未知命令:${command}`);
56
+ } catch (error) {
57
+ console.error(`\n操作失败:${error instanceof Error ? error.message : String(error)}`);
58
+ process.exitCode = 1;
59
+ }
60
+
61
+ async function initialize(options) {
62
+ const interactive = process.stdin.isTTY && !options.yes;
63
+ const rl = interactive ? createInterface({ input, output }) : null;
64
+ console.log("\nFeishu Codex Bridge 初始化\n");
65
+ printPrerequisites();
66
+ let configFile;
67
+ let installState;
68
+
69
+ try {
70
+ const instanceId = normalizeInstanceId(
71
+ options.instance ?? (await ask(rl, "实例名称", "default")),
72
+ );
73
+ const paths = defaultSetupPaths(instanceId);
74
+ configFile = path.resolve(options.config ?? paths.configFile);
75
+ const existing = await readEnvFile(configFile);
76
+ installState = await loadInstallState(configFile);
77
+ const dataDir = path.resolve(options.dataDir ?? existing.BRIDGE_DATA_DIR ?? paths.dataDir);
78
+ const detectedInstallation = await detectExistingInstallation({
79
+ configFile,
80
+ dataDir,
81
+ instanceId,
82
+ });
83
+ printExistingInstallation(existing, installState, configFile, detectedInstallation);
84
+ if (detectedInstallation.data.unsafe) {
85
+ throw new Error(`运行数据目录不是安全的普通目录:${dataDir}`);
86
+ }
87
+ installState = await recordInstallStep(configFile, installState, "environment_ready", {
88
+ instanceId,
89
+ });
90
+ const preset = await choosePreset(options.preset, rl, inferPreset(existing));
91
+ const workdir = path.resolve(
92
+ options.workdir ??
93
+ (await ask(rl, "默认项目目录", existing.CODEX_WORKDIR ?? process.cwd())),
94
+ );
95
+ await requireDirectory(workdir, "默认项目目录");
96
+ const projectRoots = splitCsv(
97
+ options.projectRoots ??
98
+ (await ask(
99
+ rl,
100
+ "额外项目扫描根目录(逗号分隔,可留空)",
101
+ existing.CODEX_PROJECT_ROOTS ?? "",
102
+ )),
103
+ );
104
+ for (const root of projectRoots) await requireDirectory(path.resolve(root), "项目扫描根目录");
105
+
106
+ if (botIdentityAvailable()) {
107
+ installState = await recordInstallStep(configFile, installState, "feishu_bound");
108
+ const capabilityReport = probeFeishuCapabilities(localBinary("lark-cli"), {
109
+ cwd: packageRoot,
110
+ });
111
+ printCapabilityReport(capabilityReport);
112
+ if (!capabilityReport.ok) {
113
+ throw new Error("飞书应用能力检查未通过,请按上方提示修复后重新运行。");
114
+ }
115
+ installState = await recordInstallStep(
116
+ configFile,
117
+ installState,
118
+ "capabilities_verified",
119
+ );
120
+ } else if (options.skipFeishuCheck) {
121
+ console.log("! 已显式跳过飞书能力检查;该模式只用于打包测试或离线准备配置。\n");
122
+ } else {
123
+ throw new Error("飞书 Bot 身份不可用,请先运行 npx lark-cli config init --new。");
124
+ }
125
+ const existingOperators = existing.ALLOWED_FEISHU_OPEN_IDS ?? "";
126
+ const discovered = await maybeDiscoverIdentity(
127
+ { ...options, openId: (options.openId ?? existingOperators) || undefined },
128
+ rl,
129
+ );
130
+ const operatorInput = options.openId ?? discovered?.senderId ?? existingOperators;
131
+ const operatorIds = validateOpenIds(
132
+ operatorInput || (await askRequired(rl, "操作者飞书 open_id(多个用逗号分隔)")),
133
+ "操作者 open_id",
134
+ );
135
+ installState = await recordInstallStep(configFile, installState, "identity_discovered", {
136
+ ...(discovered?.chatType ? { chatType: discovered.chatType } : {}),
137
+ });
138
+ const adminDefault = preset === "team" ? "" : operatorIds.join(",");
139
+ let adminInput = options.adminId ?? existing.FEISHU_ADMIN_OPEN_IDS ?? "";
140
+ if (!adminInput) {
141
+ adminInput = preset === "team"
142
+ ? await askRequired(rl, "管理员 open_id(多个用逗号分隔)")
143
+ : await ask(rl, "管理员 open_id", adminDefault);
144
+ }
145
+ const adminIds = validateOpenIds(adminInput || operatorIds, "管理员 open_id");
146
+ const viewerIds = splitCsv(
147
+ options.viewerId ??
148
+ (await ask(rl, "只读成员 open_id(可留空)", existing.FEISHU_VIEWER_OPEN_IDS ?? "")),
149
+ );
150
+ if (viewerIds.length > 0) validateOpenIds(viewerIds, "只读成员 open_id");
151
+ const discoveredGroupChat = discovered?.chatType === "group" ? discovered.chatId : "";
152
+ const chatDefault = [...new Set([
153
+ ...splitCsv(existing.ALLOWED_FEISHU_CHAT_IDS ?? ""),
154
+ ...splitCsv(discoveredGroupChat),
155
+ ])].join(",");
156
+ const chatIds = splitCsv(
157
+ options.chatId ??
158
+ (await ask(rl, "允许的群聊 chat_id(可留空)", chatDefault)),
159
+ );
160
+ validateChatIds(chatIds, "群聊 chat_id");
161
+
162
+ if (preset === "power" && !options.allowFullAccess) {
163
+ if (!rl) {
164
+ throw new Error("高级模式需要显式添加 --allow-full-access。 ");
165
+ }
166
+ const confirmation = await askRequired(
167
+ rl,
168
+ "高级模式允许管理员执行本机命令。请输入“我理解风险”继续",
169
+ );
170
+ if (confirmation !== "我理解风险") throw new Error("未确认高级模式风险。");
171
+ }
172
+ installState = await recordInstallStep(configFile, installState, "preferences_confirmed", {
173
+ preset,
174
+ });
175
+
176
+ const env = buildEnvFile({
177
+ preset,
178
+ instanceId,
179
+ dataDir,
180
+ operatorIds,
181
+ adminIds,
182
+ viewerIds,
183
+ memberLabelsJson: existing.FEISHU_MEMBER_LABELS_JSON ?? "",
184
+ chatIds,
185
+ workdir,
186
+ projectRoots,
187
+ });
188
+ const writeResult = await writeManagedConfig(configFile, env, {
189
+ forceReset: Boolean(options.forceReset || options.force),
190
+ });
191
+ await mkdir(dataDir, { recursive: true, mode: 0o700 });
192
+ await chmod(dataDir, 0o700);
193
+ installState = await recordInstallStep(configFile, installState, "config_written", {
194
+ preset,
195
+ });
196
+
197
+ console.log(`\n配置已写入:${configFile}`);
198
+ if (writeResult.backupFile) console.log(`旧配置已备份:${writeResult.backupFile}`);
199
+ console.log(`权限预设:${SETUP_PRESETS[preset].label}`);
200
+
201
+ if (!botIdentityAvailable()) {
202
+ console.log("\n飞书 Bot 身份尚未配置。请先运行:");
203
+ console.log(" npx lark-cli config init --new");
204
+ console.log("配置完成后重新运行 doctor。");
205
+ }
206
+
207
+ const doctorCode = runProjectCommand("doctor", { config: configFile }, false);
208
+
209
+ const shouldInstall = doctorCode !== 0 || options.noService
210
+ ? false
211
+ : options.yes || (await confirm(rl, "安装并启动后台服务?", true));
212
+ if (shouldInstall) {
213
+ runServiceCommand("install", { config: configFile });
214
+ const health = await waitForServiceHealth({
215
+ dataDir,
216
+ instanceId,
217
+ configFile,
218
+ timeoutMs: parseDurationMs(options.healthTimeout ?? "30s", "--health-timeout"),
219
+ });
220
+ console.log(`✓ 后台服务健康:PID ${health.pid},飞书事件连接 ${health.consumers.length} 个`);
221
+ console.log(`✓ 后台服务配置一致:${health.configFile}`);
222
+ installState = await recordInstallStep(configFile, installState, "service_running", {
223
+ serviceInstalled: true,
224
+ });
225
+ if (options.noTestCard) {
226
+ installState = await recordInstallStep(configFile, installState, "completed", {
227
+ serviceInstalled: true,
228
+ testCardDisabled: true,
229
+ });
230
+ } else if (discovered?.messageId) {
231
+ const allowGroupCard = discovered.chatType !== "group"
232
+ || options.sendTestCard
233
+ || (await confirm(rl, "向刚才的群聊发送安装成功测试卡?", false));
234
+ if (allowGroupCard) {
235
+ await sendInstallationCard({
236
+ packageRoot,
237
+ larkCliPath: localBinary("lark-cli"),
238
+ messageId: discovered.messageId,
239
+ ownerId: operatorIds[0],
240
+ instanceId,
241
+ projectName: path.basename(workdir),
242
+ sandboxLabel: sandboxLabelForPreset(preset),
243
+ });
244
+ installState = await recordInstallStep(
245
+ configFile,
246
+ installState,
247
+ "test_card_delivered",
248
+ { serviceInstalled: true },
249
+ );
250
+ installState = await recordInstallStep(configFile, installState, "completed", {
251
+ serviceInstalled: true,
252
+ });
253
+ console.log("安装成功测试卡已发送,端到端连接验证通过。");
254
+ } else {
255
+ console.log("已跳过群聊测试卡;安装状态保留为 service_running。");
256
+ }
257
+ } else {
258
+ console.log("未保留可回复的测试消息,本次未发送测试卡;安装状态保留为 service_running。");
259
+ }
260
+ }
261
+ else if (doctorCode !== 0) {
262
+ installState = await recordInstallError(configFile, installState, "doctor 检查未通过");
263
+ console.log("\n自检尚未通过,已跳过后台服务安装。修复后运行:");
264
+ console.log(` feishu-codex-bridge doctor --config ${shellQuote(configFile)}`);
265
+ console.log(` feishu-codex-bridge install --config ${shellQuote(configFile)}`);
266
+ }
267
+
268
+ console.log("\n下一步:在飞书中给机器人发送“新手引导”。");
269
+ console.log(`查看状态:feishu-codex-bridge status --config ${shellQuote(configFile)}`);
270
+ } catch (error) {
271
+ if (configFile) await recordInstallError(configFile, installState, error).catch(() => undefined);
272
+ throw error;
273
+ } finally {
274
+ rl?.close();
275
+ }
276
+ }
277
+
278
+ async function discover(options) {
279
+ if (!botIdentityAvailable()) {
280
+ throw new Error("飞书 Bot 身份不可用,请先运行 npx lark-cli config init --new。");
281
+ }
282
+ const timeout = options.timeout ?? "2m";
283
+ console.log(`请在 ${timeout} 内给机器人发送一条消息,正在等待……`);
284
+ const result = await discoverFeishuIdentity(localBinary("lark-cli"), {
285
+ timeout,
286
+ cwd: packageRoot,
287
+ });
288
+ if (!result) throw new Error("等待超时,没有收到用户消息。");
289
+ printDiscoveredIdentity(result);
290
+ console.log(JSON.stringify(result));
291
+ }
292
+
293
+ async function migrateLegacy(options) {
294
+ const interactive = process.stdin.isTTY && !options.yes;
295
+ const rl = interactive ? createInterface({ input, output }) : null;
296
+ try {
297
+ const sourceDir = path.resolve(options.from ?? process.cwd());
298
+ console.log("\n迁移旧源码安装\n");
299
+ const result = await migrateLegacyInstallation({
300
+ sourceDir,
301
+ ...(options.sourceConfig ? { sourceConfig: options.sourceConfig } : {}),
302
+ ...(options.config ? { targetConfig: options.config } : {}),
303
+ ...(options.instance ? { instance: options.instance } : {}),
304
+ allowExisting: Boolean(options.force),
305
+ });
306
+ console.log(`✓ 旧配置:${result.sourceConfig}`);
307
+ console.log(`✓ 新配置:${result.targetConfig}`);
308
+ if (result.backupFile) console.log(`✓ 原目标配置备份:${result.backupFile}`);
309
+ console.log(`✓ 继续使用旧运行数据:${result.dataDir}`);
310
+ console.log("旧源码目录和 .env 未被删除,可在新服务验证后自行归档。\n");
311
+
312
+ let installState = await loadInstallState(result.targetConfig);
313
+ installState = await recordInstallStep(
314
+ result.targetConfig,
315
+ installState,
316
+ "environment_ready",
317
+ { instanceId: result.instanceId },
318
+ );
319
+ installState = await recordInstallStep(
320
+ result.targetConfig,
321
+ installState,
322
+ "config_written",
323
+ { migration: true },
324
+ );
325
+ const doctorCode = runProjectCommand("doctor", { config: result.targetConfig }, false);
326
+ if (doctorCode !== 0) {
327
+ await recordInstallError(result.targetConfig, installState, "迁移后的 doctor 检查未通过");
328
+ console.log("迁移配置已保留,但自检未通过;修复后重新运行 install。");
329
+ return;
330
+ }
331
+ const shouldInstall = options.yes || (rl ? await confirm(rl, "切换到新后台服务?", true) : false);
332
+ if (!shouldInstall) {
333
+ console.log(`稍后运行:feishu-codex-bridge install --config ${shellQuote(result.targetConfig)}`);
334
+ return;
335
+ }
336
+ await installService({
337
+ config: result.targetConfig,
338
+ healthTimeout: options.healthTimeout ?? "30s",
339
+ });
340
+ installState = await recordInstallStep(
341
+ result.targetConfig,
342
+ installState,
343
+ "service_running",
344
+ { serviceInstalled: true, migration: true },
345
+ );
346
+ await recordInstallStep(result.targetConfig, installState, "completed", {
347
+ serviceInstalled: true,
348
+ migration: true,
349
+ testCardDisabled: true,
350
+ });
351
+ console.log("✓ 迁移完成。旧服务已由安装器替换,旧源码数据仍保留在原位置。");
352
+ } finally {
353
+ rl?.close();
354
+ }
355
+ }
356
+
357
+ async function installService(options) {
358
+ const configFile = resolveConfig(options);
359
+ const existing = await readEnvFile(configFile);
360
+ if (Object.keys(existing).length === 0) {
361
+ throw new Error(`配置文件不存在或为空:${configFile}。请先运行 init。`);
362
+ }
363
+ const instanceId = normalizeInstanceId(existing.BRIDGE_INSTANCE_ID ?? options.instance ?? "default");
364
+ const dataDir = path.resolve(packageRoot, existing.BRIDGE_DATA_DIR ?? "var");
365
+ runServiceCommand("install", { config: configFile });
366
+ const health = await waitForServiceHealth({
367
+ dataDir,
368
+ instanceId,
369
+ configFile,
370
+ timeoutMs: parseDurationMs(options.healthTimeout ?? "30s", "--health-timeout"),
371
+ });
372
+ console.log(`✓ 后台服务健康:PID ${health.pid},配置 ${health.configFile}`);
373
+ return health;
374
+ }
375
+
376
+ async function generateSupportBundle(options) {
377
+ const configFile = resolveConfig(options);
378
+ const existing = await readEnvFile(configFile);
379
+ if (Object.keys(existing).length === 0) {
380
+ throw new Error(`配置文件不存在或为空:${configFile}。请先运行 init。`);
381
+ }
382
+ const dataDir = path.resolve(packageRoot, existing.BRIDGE_DATA_DIR ?? "var");
383
+ const requested = options.output
384
+ ? path.resolve(options.output)
385
+ : path.join(
386
+ dataDir,
387
+ "diagnostics",
388
+ `support-bundle-${new Date().toISOString().replace(/[:.]/g, "-")}.json`,
389
+ );
390
+ const status = runProjectCommand(
391
+ "doctor",
392
+ { config: configFile, diagnostics: requested },
393
+ false,
394
+ );
395
+ const details = await stat(requested).catch(() => null);
396
+ if (!details?.isFile()) {
397
+ throw new Error(`诊断未能生成支持包;doctor 退出码 ${status}。`);
398
+ }
399
+ console.log(`✓ 脱敏支持包:${requested}`);
400
+ if (status !== 0) {
401
+ console.log("! 自检仍有失败项;支持包已保留,请先查看上方修复建议。");
402
+ }
403
+ }
404
+
405
+ async function printVersionInfo(options) {
406
+ const manifest = JSON.parse(await readFile(path.join(packageRoot, "package.json"), "utf8"));
407
+ const info = {
408
+ product: manifest.name,
409
+ version: manifest.version,
410
+ node: process.versions.node,
411
+ codex: manifest.dependencies?.["@openai/codex"] ?? "unknown",
412
+ larkCli: manifest.dependencies?.["@larksuite/cli"] ?? "unknown",
413
+ configContract: 1,
414
+ stateContract: 6,
415
+ sqliteSchema: 1,
416
+ platforms: ["macOS LaunchAgent", "Linux systemd user"],
417
+ };
418
+ if (options.json) {
419
+ console.log(JSON.stringify(info, null, 2));
420
+ return;
421
+ }
422
+ console.log(`${info.product} ${info.version}`);
423
+ console.log(`Node.js ${info.node} · @openai/codex ${info.codex} · @larksuite/cli ${info.larkCli}`);
424
+ console.log(
425
+ `配置 v${info.configContract} · 状态 v${info.stateContract} · SQLite schema v${info.sqliteSchema}`,
426
+ );
427
+ console.log(`平台:${info.platforms.join(" / ")}`);
428
+ }
429
+
430
+ async function initializeRunbooks(options) {
431
+ const projectDir = path.resolve(options.project ?? process.cwd());
432
+ const result = await initializeRunbookTemplate({
433
+ projectDir,
434
+ sourceFile: path.join(packageRoot, ".feishu-codex-runbooks.example.json"),
435
+ });
436
+ if (!result.created) {
437
+ console.log(`运行手册已存在,未覆盖:${result.target}`);
438
+ console.log("请先评审现有模板;删除或改名后可重新初始化。");
439
+ return;
440
+ }
441
+ console.log(`✓ 已创建运行手册:${result.target}`);
442
+ console.log("下一步:评审 prompt 与权限,然后在飞书发送“运行手册”。");
443
+ }
444
+
445
+ async function upgradeService(options) {
446
+ const configFile = resolveConfig(options);
447
+ const existing = await readEnvFile(configFile);
448
+ if (Object.keys(existing).length === 0) {
449
+ throw new Error(`配置文件不存在或为空:${configFile}。请先运行 init。`);
450
+ }
451
+ const manifest = JSON.parse(await readFile(path.join(packageRoot, "package.json"), "utf8"));
452
+ const dataDir = path.resolve(packageRoot, existing.BRIDGE_DATA_DIR ?? "var");
453
+ const databaseFile = existing.BRIDGE_DATABASE_FILE
454
+ ? path.resolve(packageRoot, existing.BRIDGE_DATABASE_FILE)
455
+ : path.join(dataDir, "state.sqlite");
456
+ const health = await readJsonOptional(path.join(dataDir, "bridge-health.json"));
457
+ const liveHealth = isLiveServiceHealth(health, configFile) ? health : null;
458
+ const databaseActiveTasks = liveHealth ? await countRunningTasks(databaseFile) : 0;
459
+ const readiness = assessUpgradeReadiness({
460
+ currentVersion: health?.productVersion ?? "unknown",
461
+ targetVersion: manifest.version,
462
+ healthActiveTasks: liveHealth?.activeTasks ?? 0,
463
+ databaseActiveTasks,
464
+ allowDowngrade: Boolean(options.allowDowngrade),
465
+ force: Boolean(options.force),
466
+ });
467
+
468
+ console.log("\nFeishu Codex Bridge 安全升级\n");
469
+ console.log(`当前服务:${readiness.currentVersion}`);
470
+ console.log(`目标版本:${readiness.targetVersion}`);
471
+ console.log(`运行任务:${Math.max(Number(liveHealth?.activeTasks ?? 0), databaseActiveTasks)}`);
472
+ if (Number(liveHealth?.queuedTasks ?? 0) > 0) {
473
+ console.log(`排队任务:${liveHealth.queuedTasks}(升级后会从 SQLite 恢复)`);
474
+ }
475
+ for (const warning of readiness.warnings) console.log(`! ${warning}`);
476
+ if (!readiness.allowed) {
477
+ if (readiness.upToDate) {
478
+ console.log(`✓ ${readiness.reason}`);
479
+ return;
480
+ }
481
+ throw new Error(readiness.reason);
482
+ }
483
+ if (!options.yes) {
484
+ console.log("\n升级将依次执行:一致性备份 → 新版本自检 → 停止旧服务 → 安装并验证新服务。");
485
+ console.log("未做任何修改。确认后重新运行并添加 --yes。");
486
+ return;
487
+ }
488
+
489
+ const backupOutput = runDataCommandCapture("backup", {
490
+ config: configFile,
491
+ reason: `upgrade-to-${manifest.version}`,
492
+ });
493
+ const backupId = parseBackupId(backupOutput);
494
+ if (!backupId) throw new Error("升级备份已执行,但无法读取备份 ID;服务未停止");
495
+ console.log(backupOutput.trim());
496
+
497
+ const doctorCode = runProjectCommand("doctor", { config: configFile }, false);
498
+ if (doctorCode !== 0) {
499
+ throw new Error(`新版本自检未通过;备份 ${backupId} 已保留,旧服务未停止`);
500
+ }
501
+
502
+ const oldPackageRoot = await validatedPreviousPackageRoot(health);
503
+ let serviceStopped = false;
504
+ try {
505
+ runServiceCommand("stop", { config: configFile });
506
+ serviceStopped = true;
507
+ const nextHealth = await installService({
508
+ config: configFile,
509
+ healthTimeout: options.healthTimeout ?? "45s",
510
+ });
511
+ if (nextHealth.productVersion && nextHealth.productVersion !== manifest.version) {
512
+ throw new Error(
513
+ `新服务报告版本 ${nextHealth.productVersion},预期 ${manifest.version}`,
514
+ );
515
+ }
516
+ console.log(`✓ 升级完成:${readiness.currentVersion} → ${manifest.version}`);
517
+ console.log(`✓ 升级前备份:${backupId}`);
518
+ } catch (error) {
519
+ if (serviceStopped) {
520
+ try {
521
+ runServiceCommand("stop", { config: configFile });
522
+ } catch {
523
+ // The failed service may already be stopped.
524
+ }
525
+ try {
526
+ const rollbackOutput = runDataCommandCapture("rollback", {
527
+ config: configFile,
528
+ backup: backupId,
529
+ yes: true,
530
+ });
531
+ console.log(rollbackOutput.trim());
532
+ } catch (rollbackError) {
533
+ console.error(`! 自动数据回滚失败:${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`);
534
+ }
535
+ try {
536
+ if (oldPackageRoot && oldPackageRoot !== packageRoot) {
537
+ installServiceFromPackage(oldPackageRoot, configFile);
538
+ const restoredHealth = await waitForServiceHealth({
539
+ dataDir,
540
+ instanceId: normalizeInstanceId(existing.BRIDGE_INSTANCE_ID ?? "default"),
541
+ configFile,
542
+ timeoutMs: parseDurationMs(options.healthTimeout ?? "45s", "--health-timeout"),
543
+ });
544
+ if (
545
+ readiness.currentVersion !== "unknown" &&
546
+ restoredHealth.productVersion &&
547
+ restoredHealth.productVersion !== readiness.currentVersion
548
+ ) {
549
+ throw new Error(
550
+ `恢复服务报告版本 ${restoredHealth.productVersion},预期 ${readiness.currentVersion}`,
551
+ );
552
+ }
553
+ console.log(`✓ 已恢复并验证旧服务包:${oldPackageRoot}`);
554
+ } else {
555
+ await installService({
556
+ config: configFile,
557
+ healthTimeout: options.healthTimeout ?? "45s",
558
+ });
559
+ console.log("✓ 已使用当前可用包恢复并验证后台服务。");
560
+ }
561
+ } catch (restoreError) {
562
+ console.error(`! 自动恢复后台服务失败:${restoreError instanceof Error ? restoreError.message : String(restoreError)}`);
563
+ }
564
+ }
565
+ throw new Error(
566
+ `升级没有完成,已尝试恢复备份 ${backupId}:${error instanceof Error ? error.message : String(error)}`,
567
+ );
568
+ }
569
+ }
570
+
571
+ async function readJsonOptional(file) {
572
+ try {
573
+ const details = await stat(file);
574
+ if (!details.isFile()) return null;
575
+ return JSON.parse(await readFile(file, "utf8"));
576
+ } catch (error) {
577
+ if (error?.code === "ENOENT") return null;
578
+ throw error;
579
+ }
580
+ }
581
+
582
+ async function countRunningTasks(databaseFile) {
583
+ const details = await stat(databaseFile).catch(() => null);
584
+ if (!details?.isFile()) return 0;
585
+ const { DatabaseSync } = await import("node:sqlite");
586
+ const database = new DatabaseSync(databaseFile, { readOnly: true });
587
+ try {
588
+ const row = database.prepare("SELECT payload FROM bridge_state WHERE id = 1").get();
589
+ const state = row?.payload ? JSON.parse(row.payload) : {};
590
+ return Object.values(state.tasks ?? {}).filter((task) => task?.status === "running").length;
591
+ } finally {
592
+ database.close();
593
+ }
594
+ }
595
+
596
+ function isLiveServiceHealth(health, configFile, now = Date.now()) {
597
+ if (!health || !Number.isSafeInteger(health.pid) || health.pid <= 0) return false;
598
+ if (path.resolve(health.configFile ?? "") !== path.resolve(configFile)) return false;
599
+ const updatedAt = Date.parse(health.updatedAt ?? "");
600
+ if (!Number.isFinite(updatedAt) || Math.abs(now - updatedAt) > 45_000) return false;
601
+ try {
602
+ process.kill(health.pid, 0);
603
+ return true;
604
+ } catch (error) {
605
+ return error?.code === "EPERM";
606
+ }
607
+ }
608
+
609
+ async function validatedPreviousPackageRoot(health) {
610
+ const candidate = health?.packageRoot;
611
+ if (!candidate || !path.isAbsolute(candidate)) return null;
612
+ const manifestFile = path.join(candidate, "package.json");
613
+ const serviceFile = path.join(candidate, "scripts", "service.mjs");
614
+ const [manifestDetails, serviceDetails] = await Promise.all([
615
+ stat(manifestFile).catch(() => null),
616
+ stat(serviceFile).catch(() => null),
617
+ ]);
618
+ if (!manifestDetails?.isFile() || !serviceDetails?.isFile()) return null;
619
+ const manifest = JSON.parse(await readFile(manifestFile, "utf8"));
620
+ if (health.productVersion && manifest.version !== health.productVersion) return null;
621
+ return path.resolve(candidate);
622
+ }
623
+
624
+ function installServiceFromPackage(root, configFile) {
625
+ const result = spawnSync(
626
+ process.execPath,
627
+ [path.join(root, "scripts", "service.mjs"), "install"],
628
+ {
629
+ cwd: root,
630
+ env: { ...process.env, DOTENV_CONFIG_PATH: configFile },
631
+ stdio: "inherit",
632
+ },
633
+ );
634
+ if (result.status !== 0) throw new Error("旧版本后台服务重新安装失败");
635
+ }
636
+
637
+ async function maybeDiscoverIdentity(options, rl) {
638
+ if (options.openId || !rl || !botIdentityAvailable()) return null;
639
+ if (!(await confirm(rl, "发送一条飞书消息,自动识别 open_id 和 chat_id?", true))) {
640
+ return null;
641
+ }
642
+ console.log("请在 2 分钟内给机器人发送一条消息,正在等待……");
643
+ try {
644
+ const result = await discoverFeishuIdentity(localBinary("lark-cli"), {
645
+ timeout: options.discoveryTimeout ?? "2m",
646
+ cwd: packageRoot,
647
+ });
648
+ if (!result) {
649
+ console.log("未收到消息,继续手工填写。");
650
+ return null;
651
+ }
652
+ printDiscoveredIdentity(result);
653
+ return result;
654
+ } catch (error) {
655
+ console.log(`自动识别失败:${error instanceof Error ? error.message : String(error)}`);
656
+ console.log("继续手工填写。");
657
+ return null;
658
+ }
659
+ }
660
+
661
+ function printDiscoveredIdentity(result) {
662
+ console.log(`✓ 已识别用户:${result.senderId}`);
663
+ console.log(`✓ 会话:${result.chatId}(${result.chatType === "group" ? "群聊" : "私聊"})`);
664
+ }
665
+
666
+ function printPrerequisites() {
667
+ const nodeMajor = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
668
+ console.log(`${nodeMajor >= 22 ? "✓" : "✗"} Node.js ${process.versions.node}(需要 22+)`);
669
+ if (nodeMajor < 22) throw new Error("Node.js 版本过低,请升级到 22 或更高版本。");
670
+ console.log(`${commandAvailable(localBinary("codex"), ["--version"]) ? "✓" : "!"} Codex CLI`);
671
+ console.log(`${botIdentityAvailable() ? "✓" : "!"} 飞书 Bot 身份`);
672
+ }
673
+
674
+ async function choosePreset(value, rl, fallback = "personal") {
675
+ if (value) {
676
+ if (!SETUP_PRESETS[value]) throw new Error("--preset 支持 personal、team 或 power。");
677
+ return value;
678
+ }
679
+ if (!rl) return fallback;
680
+ console.log("\n权限预设:");
681
+ console.log(" 1. 个人安全(推荐):工作区写入,网络关闭");
682
+ console.log(" 2. 团队安全:成员隔离,显式管理员");
683
+ console.log(" 3. 高级模式:管理员完全访问,操作者仍为工作区写入");
684
+ const defaultChoice = { personal: "1", team: "2", power: "3" }[fallback] ?? "1";
685
+ const answer = await ask(rl, "选择", defaultChoice);
686
+ return { "1": "personal", "2": "team", "3": "power" }[answer] ?? choosePreset(answer, null);
687
+ }
688
+
689
+ function inferPreset(existing) {
690
+ if (existing.CODEX_SANDBOX_MODE === "danger-full-access") return "power";
691
+ const operators = splitCsv(existing.ALLOWED_FEISHU_OPEN_IDS ?? "");
692
+ const admins = splitCsv(existing.FEISHU_ADMIN_OPEN_IDS ?? "");
693
+ const viewers = splitCsv(existing.FEISHU_VIEWER_OPEN_IDS ?? "");
694
+ if (viewers.length > 0 || admins.some((id) => !operators.includes(id))) return "team";
695
+ return "personal";
696
+ }
697
+
698
+ function printExistingInstallation(existing, state, configFile, detected) {
699
+ if (Object.keys(existing).length === 0 && !state && !detected?.exists) return;
700
+ console.log(`检测到已有安装:${configFile}`);
701
+ if (state?.status) console.log(`上次安装进度:${state.status}`);
702
+ if (detected?.service.installed) {
703
+ console.log(`后台服务:${detected.service.running ? "正在运行" : "已安装但未运行"}(${detected.service.name})`);
704
+ }
705
+ if (detected?.data.exists) {
706
+ const safety = detected.data.unsafe ? ",路径不安全" : "";
707
+ console.log(`运行数据:${detected.data.entryCount} 项${safety}(${detected.data.path})`);
708
+ }
709
+ if (detected?.health.exists) {
710
+ console.log(`最近健康状态:${detected.health.status ?? "unknown"}${detected.health.pid ? `(PID ${detected.health.pid})` : ""}`);
711
+ }
712
+ console.log("已知配置会更新,未知高级配置会保留,并在写入前创建备份。\n");
713
+ }
714
+
715
+ function printCapabilityReport(report) {
716
+ console.log("\n飞书应用能力检查:");
717
+ for (const check of report.checks) {
718
+ console.log(`${check.ok ? "✓" : "✗"} ${check.label}:${check.detail}`);
719
+ if (!check.ok && check.remediation) console.log(` 修复:${check.remediation}`);
720
+ }
721
+ console.log("");
722
+ }
723
+
724
+ function sandboxLabelForPreset(preset) {
725
+ return preset === "power" ? "完全访问(管理员)" : "工作区写入";
726
+ }
727
+
728
+ function runProjectCommand(name, options, throwOnFailure = true) {
729
+ const configFile = resolveConfig(options);
730
+ const target = name === "doctor" ? path.join(packageRoot, "dist", "doctor.js") : null;
731
+ if (!target) throw new Error(`未知项目命令:${name}`);
732
+ const commandArgs = [target];
733
+ if (name === "doctor") {
734
+ if (options.fix) commandArgs.push("--fix");
735
+ if (options.diagnostics) commandArgs.push("--diagnostics", path.resolve(options.diagnostics));
736
+ }
737
+ const result = spawnSync(process.execPath, commandArgs, {
738
+ cwd: packageRoot,
739
+ env: { ...process.env, DOTENV_CONFIG_PATH: configFile },
740
+ stdio: "inherit",
741
+ });
742
+ if (throwOnFailure && result.status !== 0) {
743
+ throw new Error(`${name} 检查未通过,请按上方提示修复。`);
744
+ }
745
+ return result.status ?? 1;
746
+ }
747
+
748
+ function runServiceCommand(action, options) {
749
+ const configFile = resolveConfig(options);
750
+ const result = spawnSync(process.execPath, [path.join(packageRoot, "scripts", "service.mjs"), action], {
751
+ cwd: packageRoot,
752
+ env: { ...process.env, DOTENV_CONFIG_PATH: configFile },
753
+ stdio: "inherit",
754
+ });
755
+ if (result.status !== 0) throw new Error(`后台服务命令 ${action} 执行失败。`);
756
+ }
757
+
758
+ function runDataCommand(action, options) {
759
+ const configFile = resolveConfig(options);
760
+ const commandArgs = [path.join(packageRoot, "dist", "data-maintenance.js"), action];
761
+ if (options.backup) commandArgs.push("--backup", options.backup);
762
+ if (options.reason) commandArgs.push("--reason", options.reason);
763
+ if (options.yes) commandArgs.push("--yes");
764
+ const result = spawnSync(process.execPath, commandArgs, {
765
+ cwd: packageRoot,
766
+ env: { ...process.env, DOTENV_CONFIG_PATH: configFile },
767
+ stdio: "inherit",
768
+ });
769
+ if (result.status !== 0) throw new Error(`数据维护命令 ${action} 执行失败。`);
770
+ }
771
+
772
+ function runDataCommandCapture(action, options) {
773
+ const configFile = resolveConfig(options);
774
+ const commandArgs = [path.join(packageRoot, "dist", "data-maintenance.js"), action];
775
+ if (options.backup) commandArgs.push("--backup", options.backup);
776
+ if (options.reason) commandArgs.push("--reason", options.reason);
777
+ if (options.yes) commandArgs.push("--yes");
778
+ const result = spawnSync(process.execPath, commandArgs, {
779
+ cwd: packageRoot,
780
+ env: { ...process.env, DOTENV_CONFIG_PATH: configFile },
781
+ encoding: "utf8",
782
+ stdio: "pipe",
783
+ });
784
+ if (result.status !== 0) {
785
+ throw new Error(
786
+ `数据维护命令 ${action} 执行失败:${result.stderr || result.stdout || "未知错误"}`,
787
+ );
788
+ }
789
+ return result.stdout;
790
+ }
791
+
792
+ function resolveConfig(options) {
793
+ if (options.config) return path.resolve(options.config);
794
+ const instance = normalizeInstanceId(options.instance ?? "default");
795
+ return defaultSetupPaths(instance).configFile;
796
+ }
797
+
798
+ function botIdentityAvailable() {
799
+ return commandAvailable(localBinary("lark-cli"), ["whoami", "--as", "bot"]);
800
+ }
801
+
802
+ function commandAvailable(command, commandArgs) {
803
+ const result = spawnSync(command, commandArgs, {
804
+ cwd: packageRoot,
805
+ env: process.env,
806
+ stdio: "ignore",
807
+ });
808
+ return result.status === 0;
809
+ }
810
+
811
+ function localBinary(name) {
812
+ return path.join(packageRoot, "node_modules", ".bin", name);
813
+ }
814
+
815
+ async function requireDirectory(directory, label) {
816
+ const details = await stat(directory).catch(() => null);
817
+ if (!details?.isDirectory()) throw new Error(`${label}不存在:${directory}`);
818
+ }
819
+
820
+ async function ask(rl, label, fallback) {
821
+ if (!rl) return fallback;
822
+ const suffix = fallback ? ` [${fallback}]` : "";
823
+ return (await rl.question(`${label}${suffix}:`)).trim() || fallback;
824
+ }
825
+
826
+ async function askRequired(rl, label) {
827
+ if (!rl) throw new Error(`非交互模式缺少:${label}`);
828
+ while (true) {
829
+ const value = (await rl.question(`${label}:`)).trim();
830
+ if (value) return value;
831
+ console.log("该项不能为空。");
832
+ }
833
+ }
834
+
835
+ async function confirm(rl, label, fallback) {
836
+ if (!rl) return fallback;
837
+ const answer = (await rl.question(`${label} ${fallback ? "[Y/n]" : "[y/N]"}:`)).trim().toLowerCase();
838
+ if (!answer) return fallback;
839
+ return answer === "y" || answer === "yes";
840
+ }
841
+
842
+ function parseFlags(values) {
843
+ const parsed = {};
844
+ for (let index = 0; index < values.length; index += 1) {
845
+ const token = values[index];
846
+ if (!token?.startsWith("--")) throw new Error(`无法识别参数:${token}`);
847
+ const [rawKey, inlineValue] = token.slice(2).split("=", 2);
848
+ const key = rawKey.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
849
+ if (["yes", "force", "forceReset", "noService", "noTestCard", "sendTestCard", "skipFeishuCheck", "allowFullAccess", "allowDowngrade", "fix", "json"].includes(key)) {
850
+ parsed[key] = inlineValue === undefined ? true : inlineValue !== "false";
851
+ continue;
852
+ }
853
+ const value = inlineValue ?? values[++index];
854
+ if (!value || value.startsWith("--")) throw new Error(`参数 --${rawKey} 缺少值。`);
855
+ parsed[key] = value;
856
+ }
857
+ return parsed;
858
+ }
859
+
860
+ function parseDurationMs(value, label) {
861
+ const match = String(value).trim().match(/^(\d+)(ms|s|m)?$/i);
862
+ if (!match) throw new Error(`${label} 需要使用 500ms、30s 或 2m 格式。`);
863
+ const amount = Number.parseInt(match[1], 10);
864
+ const unit = (match[2] ?? "ms").toLowerCase();
865
+ const multiplier = unit === "m" ? 60_000 : unit === "s" ? 1_000 : 1;
866
+ const milliseconds = amount * multiplier;
867
+ if (!Number.isSafeInteger(milliseconds) || milliseconds <= 0) {
868
+ throw new Error(`${label} 必须大于 0。`);
869
+ }
870
+ return milliseconds;
871
+ }
872
+
873
+ function shellQuote(value) {
874
+ return `'${value.replaceAll("'", "'\\''")}'`;
875
+ }
876
+
877
+ function printHelp() {
878
+ console.log(`Feishu Codex Bridge
879
+
880
+ 用法:
881
+ feishu-codex-bridge init [选项]
882
+ feishu-codex-bridge migrate --from <旧源码目录> [选项]
883
+ feishu-codex-bridge discover [--timeout 2m]
884
+ feishu-codex-bridge doctor [--config <文件>] [--fix] [--diagnostics <JSON 文件>]
885
+ feishu-codex-bridge install|status|stop|restart|uninstall [--config <文件>]
886
+ feishu-codex-bridge backup|backups [--config <文件>]
887
+ feishu-codex-bridge rollback --backup <备份 ID> --yes [--config <文件>]
888
+ feishu-codex-bridge support-bundle [--output <JSON 文件>] [--config <文件>]
889
+ feishu-codex-bridge version [--json]
890
+ feishu-codex-bridge init-runbooks [--project <项目目录>]
891
+ feishu-codex-bridge upgrade [--config <文件>] [--yes] [--allow-downgrade]
892
+
893
+ 初始化选项:
894
+ --preset personal|team|power
895
+ --open-id ou_xxx[,ou_yyy]
896
+ --admin-id ou_xxx[,ou_yyy]
897
+ --viewer-id ou_xxx[,ou_yyy]
898
+ --chat-id oc_xxx[,oc_yyy]
899
+ --workdir <项目目录>
900
+ --project-roots <目录[,目录]>
901
+ --instance <名称>
902
+ --config <配置文件>
903
+ --data-dir <运行数据目录>
904
+ --yes --no-service --no-test-card
905
+ --send-test-card
906
+ --force-reset
907
+ --allow-full-access
908
+ --discovery-timeout 2m
909
+ --health-timeout 30s
910
+
911
+ 迁移选项:
912
+ --from <旧源码目录>
913
+ --source-config <旧 .env 文件>
914
+ --config <新配置文件>
915
+ --instance <名称>
916
+ --force(目标已存在时备份并合并)
917
+ --yes(自检通过后切换后台服务)
918
+ `);
919
+ }