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,302 @@
1
+ import { createHash } from "node:crypto";
2
+ import { rmSync } from "node:fs";
3
+ import { chmod, copyFile, lstat, mkdir, readFile, readdir, rename, rm, writeFile, } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { backup, DatabaseSync } from "node:sqlite";
6
+ import { readBridgeHealth } from "./health-file.js";
7
+ export const BACKUP_FORMAT_VERSION = 1;
8
+ export const DEFAULT_BACKUP_RETENTION = 10;
9
+ export async function createRuntimeBackup(databaseFile, options = {}) {
10
+ await assertRegularFile(databaseFile, "状态数据库");
11
+ const source = new DatabaseSync(databaseFile, { readOnly: true });
12
+ try {
13
+ return await createRuntimeBackupFromDatabase(source, databaseFile, options);
14
+ }
15
+ finally {
16
+ source.close();
17
+ }
18
+ }
19
+ export async function createRuntimeBackupFromDatabase(source, databaseFile, options = {}) {
20
+ const integrity = quickCheck(source);
21
+ if (integrity !== "ok")
22
+ throw new Error(`状态数据库完整性检查失败:${integrity}`);
23
+ const backupRoot = runtimeBackupRoot(databaseFile);
24
+ await ensurePrivateDirectory(backupRoot);
25
+ const now = options.now ?? new Date();
26
+ const reason = normalizeReason(options.reason ?? "manual");
27
+ const id = `${timestamp(now)}-${reason}-${process.pid}`;
28
+ const directory = path.join(backupRoot, id);
29
+ await mkdir(directory, { mode: 0o700 });
30
+ await chmod(directory, 0o700);
31
+ const databaseTarget = path.join(directory, "state.sqlite");
32
+ const manifestFile = path.join(directory, "manifest.json");
33
+ try {
34
+ await backup(source, databaseTarget);
35
+ await chmod(databaseTarget, 0o600);
36
+ const verified = inspectDatabase(databaseTarget);
37
+ if (verified.integrity !== "ok") {
38
+ throw new Error(`备份完整性检查失败:${verified.integrity}`);
39
+ }
40
+ const details = await lstat(databaseTarget);
41
+ const manifest = {
42
+ formatVersion: BACKUP_FORMAT_VERSION,
43
+ id,
44
+ createdAt: now.toISOString(),
45
+ reason,
46
+ database: {
47
+ file: "state.sqlite",
48
+ bytes: details.size,
49
+ sha256: await sha256File(databaseTarget),
50
+ integrity: verified.integrity,
51
+ sqliteUserVersion: verified.sqliteUserVersion,
52
+ ...(verified.stateVersion === undefined ? {} : { stateVersion: verified.stateVersion }),
53
+ },
54
+ };
55
+ await writeFile(manifestFile, `${JSON.stringify(manifest, null, 2)}\n`, {
56
+ encoding: "utf8",
57
+ mode: 0o600,
58
+ flag: "wx",
59
+ });
60
+ await chmod(manifestFile, 0o600);
61
+ const result = { id, directory, databaseFile: databaseTarget, manifestFile, manifest };
62
+ await pruneRuntimeBackups(databaseFile, options.retention ?? DEFAULT_BACKUP_RETENTION, [
63
+ id,
64
+ ...(options.protectedIds ?? []),
65
+ ]);
66
+ return result;
67
+ }
68
+ catch (error) {
69
+ await rm(directory, { recursive: true, force: true });
70
+ throw error;
71
+ }
72
+ }
73
+ export async function listRuntimeBackups(databaseFile) {
74
+ const root = runtimeBackupRoot(databaseFile);
75
+ let entries;
76
+ try {
77
+ entries = await readdir(root, { withFileTypes: true });
78
+ }
79
+ catch (error) {
80
+ if (error.code === "ENOENT")
81
+ return [];
82
+ throw error;
83
+ }
84
+ const backups = await Promise.all(entries
85
+ .filter((entry) => entry.isDirectory() && !entry.isSymbolicLink())
86
+ .map((entry) => readRuntimeBackup(path.join(root, entry.name)).catch(() => null)));
87
+ return backups
88
+ .filter((entry) => entry !== null)
89
+ .sort((left, right) => right.manifest.createdAt.localeCompare(left.manifest.createdAt));
90
+ }
91
+ export async function resolveRuntimeBackup(databaseFile, selector) {
92
+ if (!selector || selector.includes("/") || selector.includes("\\") || selector === "." || selector === "..") {
93
+ throw new Error("备份标识无效");
94
+ }
95
+ const directory = path.join(runtimeBackupRoot(databaseFile), selector);
96
+ const resolved = await readRuntimeBackup(directory);
97
+ if (resolved.id !== selector)
98
+ throw new Error("备份清单与目录不匹配");
99
+ return resolved;
100
+ }
101
+ export async function restoreRuntimeBackup(databaseFile, dataDir, selector) {
102
+ await assertBridgeStopped(dataDir);
103
+ const selected = await resolveRuntimeBackup(databaseFile, selector);
104
+ await verifyRuntimeBackup(selected);
105
+ let safetyBackup;
106
+ try {
107
+ await assertRegularFile(databaseFile, "当前状态数据库");
108
+ safetyBackup = await createRuntimeBackup(databaseFile, {
109
+ reason: "before-rollback",
110
+ protectedIds: [selected.id],
111
+ });
112
+ }
113
+ catch (error) {
114
+ if (error.code !== "ENOENT")
115
+ throw error;
116
+ }
117
+ await restoreRuntimeBackupFile(selected.databaseFile, databaseFile);
118
+ return { restored: selected, ...(safetyBackup ? { safetyBackup } : {}) };
119
+ }
120
+ export async function restoreRuntimeBackupFile(backupDatabaseFile, databaseFile) {
121
+ await assertRegularFile(backupDatabaseFile, "备份数据库");
122
+ const inspected = inspectDatabase(backupDatabaseFile);
123
+ if (inspected.integrity !== "ok") {
124
+ throw new Error(`拒绝恢复损坏的备份:${inspected.integrity}`);
125
+ }
126
+ await ensurePrivateDirectory(path.dirname(databaseFile));
127
+ const staging = `${databaseFile}.restore-${process.pid}-${Date.now()}`;
128
+ const movedSidecars = [];
129
+ let swapped = false;
130
+ try {
131
+ await copyFile(backupDatabaseFile, staging);
132
+ await chmod(staging, 0o600);
133
+ const staged = inspectDatabase(staging);
134
+ if (staged.integrity !== "ok")
135
+ throw new Error(`恢复暂存文件损坏:${staged.integrity}`);
136
+ for (const suffix of ["-wal", "-shm"]) {
137
+ const original = `${databaseFile}${suffix}`;
138
+ try {
139
+ const details = await lstat(original);
140
+ if (!details.isFile() || details.isSymbolicLink()) {
141
+ throw new Error(`SQLite 辅助文件路径不安全:${original}`);
142
+ }
143
+ const temporary = `${original}.before-restore-${process.pid}-${Date.now()}`;
144
+ await rename(original, temporary);
145
+ movedSidecars.push({ original, temporary });
146
+ }
147
+ catch (error) {
148
+ if (error.code !== "ENOENT")
149
+ throw error;
150
+ }
151
+ }
152
+ await rename(staging, databaseFile);
153
+ swapped = true;
154
+ await chmod(databaseFile, 0o600);
155
+ }
156
+ catch (error) {
157
+ if (!swapped) {
158
+ for (const sidecar of movedSidecars.toReversed()) {
159
+ await rename(sidecar.temporary, sidecar.original).catch(() => undefined);
160
+ }
161
+ }
162
+ throw error;
163
+ }
164
+ finally {
165
+ await rm(staging, { force: true });
166
+ if (swapped) {
167
+ await Promise.all(movedSidecars.map((sidecar) => rm(sidecar.temporary, { force: true })));
168
+ }
169
+ }
170
+ }
171
+ export function runtimeBackupRoot(databaseFile) {
172
+ return path.join(path.dirname(path.resolve(databaseFile)), "backups");
173
+ }
174
+ async function readRuntimeBackup(directory) {
175
+ const details = await lstat(directory);
176
+ if (!details.isDirectory() || details.isSymbolicLink())
177
+ throw new Error("备份目录不安全");
178
+ const manifestFile = path.join(directory, "manifest.json");
179
+ const databaseFile = path.join(directory, "state.sqlite");
180
+ await assertRegularFile(manifestFile, "备份清单");
181
+ await assertRegularFile(databaseFile, "备份数据库");
182
+ const manifest = JSON.parse(await readFile(manifestFile, "utf8"));
183
+ if (manifest.formatVersion !== BACKUP_FORMAT_VERSION ||
184
+ typeof manifest.id !== "string" ||
185
+ typeof manifest.createdAt !== "string" ||
186
+ typeof manifest.reason !== "string" ||
187
+ manifest.database?.file !== "state.sqlite" ||
188
+ typeof manifest.database.sha256 !== "string") {
189
+ throw new Error("备份清单格式不受支持");
190
+ }
191
+ return { id: manifest.id, directory, databaseFile, manifestFile, manifest };
192
+ }
193
+ async function verifyRuntimeBackup(item) {
194
+ const digest = await sha256File(item.databaseFile);
195
+ if (digest !== item.manifest.database.sha256)
196
+ throw new Error("备份校验和不匹配");
197
+ const inspected = inspectDatabase(item.databaseFile);
198
+ if (inspected.integrity !== "ok")
199
+ throw new Error(`备份数据库损坏:${inspected.integrity}`);
200
+ }
201
+ async function pruneRuntimeBackups(databaseFile, retention, protectedIds = []) {
202
+ const safeRetention = Math.max(1, Math.min(100, Math.trunc(retention)));
203
+ const backups = await listRuntimeBackups(databaseFile);
204
+ const protectedSet = new Set(protectedIds);
205
+ const protectedBackups = backups.filter((entry) => protectedSet.has(entry.id));
206
+ const keep = new Set([
207
+ ...protectedBackups.map((entry) => entry.id),
208
+ ...backups
209
+ .filter((entry) => !protectedSet.has(entry.id))
210
+ .slice(0, Math.max(0, safeRetention - protectedBackups.length))
211
+ .map((entry) => entry.id),
212
+ ]);
213
+ for (const stale of backups.filter((entry) => !keep.has(entry.id))) {
214
+ await rm(stale.directory, { recursive: true, force: true });
215
+ }
216
+ }
217
+ async function assertBridgeStopped(dataDir) {
218
+ const health = await readBridgeHealth(dataDir).catch(() => null);
219
+ if (!health)
220
+ return;
221
+ if (processIsAlive(health.pid)) {
222
+ throw new Error(`后台服务仍在运行(PID ${health.pid}),请先停止服务再回滚`);
223
+ }
224
+ }
225
+ async function ensurePrivateDirectory(directory) {
226
+ try {
227
+ const details = await lstat(directory);
228
+ if (!details.isDirectory() || details.isSymbolicLink()) {
229
+ throw new Error(`目录路径不安全:${directory}`);
230
+ }
231
+ }
232
+ catch (error) {
233
+ if (error.code !== "ENOENT")
234
+ throw error;
235
+ await mkdir(directory, { recursive: true, mode: 0o700 });
236
+ }
237
+ await chmod(directory, 0o700);
238
+ }
239
+ async function assertRegularFile(file, label) {
240
+ const details = await lstat(file);
241
+ if (!details.isFile() || details.isSymbolicLink())
242
+ throw new Error(`${label}路径不安全:${file}`);
243
+ }
244
+ function inspectDatabase(file) {
245
+ const database = new DatabaseSync(file, { readOnly: true });
246
+ try {
247
+ const integrity = quickCheck(database);
248
+ const userVersion = database.prepare("PRAGMA user_version").get();
249
+ let stateVersion;
250
+ try {
251
+ const row = database.prepare("SELECT version FROM bridge_state WHERE id = 1").get();
252
+ if (typeof row?.version === "number")
253
+ stateVersion = row.version;
254
+ }
255
+ catch {
256
+ // A pre-schema database can still be backed up and restored.
257
+ }
258
+ return {
259
+ integrity,
260
+ sqliteUserVersion: Number(userVersion?.user_version ?? 0),
261
+ ...(stateVersion === undefined ? {} : { stateVersion }),
262
+ };
263
+ }
264
+ finally {
265
+ database.close();
266
+ // Opening a WAL-mode snapshot for a read-only integrity check can create
267
+ // empty -wal/-shm helpers. They are not part of the backup format and must
268
+ // not be mistaken for additional restore inputs.
269
+ rmSync(`${file}-wal`, { force: true });
270
+ rmSync(`${file}-shm`, { force: true });
271
+ }
272
+ }
273
+ function quickCheck(database) {
274
+ const row = database.prepare("PRAGMA quick_check").get();
275
+ return row?.quick_check ?? "unknown";
276
+ }
277
+ async function sha256File(file) {
278
+ return createHash("sha256").update(await readFile(file)).digest("hex");
279
+ }
280
+ function normalizeReason(value) {
281
+ const normalized = value
282
+ .trim()
283
+ .toLocaleLowerCase()
284
+ .replace(/[^a-z0-9_-]+/g, "-")
285
+ .replace(/^-+|-+$/g, "")
286
+ .slice(0, 48);
287
+ return normalized || "manual";
288
+ }
289
+ function timestamp(value) {
290
+ return value.toISOString().replace(/[:.]/g, "-");
291
+ }
292
+ function processIsAlive(pid) {
293
+ if (!Number.isSafeInteger(pid) || pid <= 0)
294
+ return false;
295
+ try {
296
+ process.kill(pid, 0);
297
+ return true;
298
+ }
299
+ catch (error) {
300
+ return error.code === "EPERM";
301
+ }
302
+ }