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,182 @@
1
+ export function renderProjectOverviewCard(snapshot) {
2
+ const gitLabel = snapshot.gitStatus
3
+ ? snapshot.gitStatus.dirty
4
+ ? `${snapshot.gitStatus.changedFiles} 个变更`
5
+ : "工作区干净"
6
+ : snapshot.project.isGitRepository
7
+ ? "Git 状态未知"
8
+ : "普通文件夹";
9
+ const branch = snapshot.gitStatus?.branch ?? "—";
10
+ const stack = snapshot.stack.length > 0 ? snapshot.stack.join(" · ") : "未从 package.json 识别";
11
+ const languages = snapshot.languages.length > 0 ? snapshot.languages.join(" · ") : "暂未识别";
12
+ const directories = snapshot.topDirectories.length > 0
13
+ ? snapshot.topDirectories.map((directory) => `\`${safeMarkdown(directory)}\``).join(" ")
14
+ : "暂无目录索引";
15
+ return {
16
+ schema: "2.0",
17
+ config: {
18
+ update_multi: true,
19
+ width_mode: "default",
20
+ enable_forward: false,
21
+ summary: { content: `项目概览 · ${safePlain(snapshot.project.name)}` },
22
+ },
23
+ header: {
24
+ title: { tag: "plain_text", content: `${safePlain(snapshot.project.name)} · 项目概览` },
25
+ subtitle: { tag: "plain_text", content: "本地确定性快照,不启动 Codex" },
26
+ template: "blue",
27
+ icon: { tag: "standard_icon", token: "folder_outlined" },
28
+ text_tag_list: [
29
+ {
30
+ tag: "text_tag",
31
+ text: { tag: "plain_text", content: "0 AI token" },
32
+ color: "green",
33
+ },
34
+ {
35
+ tag: "text_tag",
36
+ text: {
37
+ tag: "plain_text",
38
+ content: snapshot.project.isGitRepository ? "Git" : "文件夹",
39
+ },
40
+ color: "blue",
41
+ },
42
+ ],
43
+ },
44
+ body: {
45
+ direction: "vertical",
46
+ padding: "12px 12px 20px 12px",
47
+ vertical_spacing: "12px",
48
+ elements: [
49
+ {
50
+ tag: "column_set",
51
+ element_id: "overview_summary",
52
+ flex_mode: "none",
53
+ columns: [
54
+ {
55
+ tag: "column",
56
+ width: "weighted",
57
+ weight: 1,
58
+ background_style: "blue-50",
59
+ padding: "12px 12px 12px 12px",
60
+ vertical_spacing: "6px",
61
+ elements: [
62
+ {
63
+ tag: "markdown",
64
+ element_id: "overview_description",
65
+ content: `**项目说明**\n${safeMarkdown(snapshot.description)}`,
66
+ },
67
+ {
68
+ tag: "markdown",
69
+ element_id: "overview_path",
70
+ text_size: "notation",
71
+ content: `<font color='grey'>路径</font> ${safeMarkdown(snapshot.project.displayPath)}`,
72
+ },
73
+ ],
74
+ },
75
+ ],
76
+ },
77
+ {
78
+ tag: "column_set",
79
+ element_id: "overview_metrics",
80
+ flex_mode: "none",
81
+ horizontal_spacing: "8px",
82
+ columns: [
83
+ metric("文件", `${formatNumber(snapshot.fileCount)}${snapshot.indexTruncated ? "+" : ""}`),
84
+ metric("分支", branch),
85
+ metric("状态", gitLabel),
86
+ ],
87
+ },
88
+ {
89
+ tag: "column_set",
90
+ element_id: "overview_tech",
91
+ flex_mode: "none",
92
+ columns: [
93
+ {
94
+ tag: "column",
95
+ width: "weighted",
96
+ weight: 1,
97
+ background_style: "grey-50",
98
+ padding: "12px 12px 12px 12px",
99
+ vertical_spacing: "6px",
100
+ elements: [
101
+ {
102
+ tag: "markdown",
103
+ element_id: "overview_stack",
104
+ content: `**技术概况**\n<font color='grey'>技术栈</font> ${safeMarkdown(stack)}\n<font color='grey'>语言</font> ${safeMarkdown(languages)}\n<font color='grey'>包管理</font> ${safeMarkdown(snapshot.packageManager)}${snapshot.workspace ? " · Workspace" : ""}${snapshot.commit ? `\n<font color='grey'>Commit</font> \`${safeMarkdown(snapshot.commit)}\`` : ""}`,
105
+ },
106
+ {
107
+ tag: "markdown",
108
+ element_id: "overview_dirs",
109
+ content: `**主要目录**\n${directories}`,
110
+ },
111
+ ],
112
+ },
113
+ ],
114
+ },
115
+ {
116
+ tag: "markdown",
117
+ element_id: "overview_hint",
118
+ text_size: "notation",
119
+ text_align: "center",
120
+ content: "<font color='grey'>“读取项目”只生成本地快照;发送“深入分析项目架构”才会启动 Codex。</font>",
121
+ },
122
+ ],
123
+ },
124
+ };
125
+ }
126
+ export function renderProjectOverviewText(snapshot) {
127
+ const git = snapshot.gitStatus
128
+ ? `${snapshot.gitStatus.branch} · ${snapshot.gitStatus.dirty ? `${snapshot.gitStatus.changedFiles} 个变更` : "工作区干净"}`
129
+ : snapshot.project.isGitRepository ? "Git 状态未知" : "普通文件夹";
130
+ return [
131
+ `项目概览 · ${snapshot.project.name}`,
132
+ snapshot.description,
133
+ "",
134
+ `- 路径:${snapshot.project.displayPath}`,
135
+ `- Git:${git}`,
136
+ `- 文件:${snapshot.fileCount}${snapshot.indexTruncated ? "+" : ""}`,
137
+ `- 技术栈:${snapshot.stack.join("、") || "未识别"}`,
138
+ `- 语言:${snapshot.languages.join("、") || "未识别"}`,
139
+ `- 包管理:${snapshot.packageManager}${snapshot.workspace ? " · Workspace" : ""}`,
140
+ `- 主要目录:${snapshot.topDirectories.join("、") || "暂无"}`,
141
+ "",
142
+ "本次使用本地快照,AI token:0。发送“深入分析项目架构”才会启动 Codex。",
143
+ ].join("\n");
144
+ }
145
+ function metric(label, value) {
146
+ return {
147
+ tag: "column",
148
+ width: "weighted",
149
+ weight: 1,
150
+ background_style: "grey-50",
151
+ padding: "8px 4px 8px 4px",
152
+ vertical_spacing: "2px",
153
+ elements: [
154
+ {
155
+ tag: "markdown",
156
+ text_align: "center",
157
+ text_size: "notation",
158
+ content: `<font color='grey'>${safeMarkdown(label)}</font>`,
159
+ },
160
+ {
161
+ tag: "markdown",
162
+ text_align: "center",
163
+ content: `**${safeMarkdown(truncate(value, 24))}**`,
164
+ },
165
+ ],
166
+ };
167
+ }
168
+ function safeMarkdown(value) {
169
+ return value
170
+ .replaceAll("\\", "\\\\")
171
+ .replace(/[<>&]/g, (character) => ({ "<": "<", ">": ">", "&": "&" })[character])
172
+ .replaceAll("`", "ˋ");
173
+ }
174
+ function safePlain(value) {
175
+ return value.replace(/[\r\n\u0000-\u001f\u007f]/g, " ").slice(0, 80);
176
+ }
177
+ function truncate(value, max) {
178
+ return value.length <= max ? value : `${value.slice(0, max - 1)}…`;
179
+ }
180
+ function formatNumber(value) {
181
+ return new Intl.NumberFormat("zh-CN").format(value);
182
+ }
@@ -0,0 +1,278 @@
1
+ import { execFile } from "node:child_process";
2
+ import { access, readFile, readdir, stat } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { promisify } from "node:util";
5
+ import { readProjectGitStatus } from "./project-status.js";
6
+ const execFileAsync = promisify(execFile);
7
+ const MAX_METADATA_BYTES = 256 * 1024;
8
+ const MAX_INDEX_BYTES = 4 * 1024 * 1024;
9
+ const MAX_FILES = 20_000;
10
+ const SKIP_DIRECTORIES = new Set([
11
+ ".git",
12
+ ".idea",
13
+ ".vscode",
14
+ "node_modules",
15
+ "dist",
16
+ "build",
17
+ "coverage",
18
+ "vendor",
19
+ ".next",
20
+ ".nuxt",
21
+ ]);
22
+ export async function readProjectOverview(project) {
23
+ const [gitStatus, commit, packageMetadata, readme, indexedFiles] = await Promise.all([
24
+ project.isGitRepository ? readProjectGitStatus(project.path) : Promise.resolve(null),
25
+ project.isGitRepository ? readGitCommit(project.path) : Promise.resolve(null),
26
+ readPackageMetadata(project.path),
27
+ readReadme(project.path),
28
+ project.isGitRepository
29
+ ? readGitFiles(project.path)
30
+ : readDirectoryFiles(project.path),
31
+ ]);
32
+ const workspacePackages = await readWorkspacePackageMetadata(project.path, indexedFiles.files);
33
+ const dependencyNames = new Set([
34
+ ...Object.keys(packageMetadata?.dependencies ?? {}),
35
+ ...Object.keys(packageMetadata?.devDependencies ?? {}),
36
+ ...workspacePackages.flatMap((metadata) => Object.keys(metadata.dependencies ?? {})),
37
+ ...workspacePackages.flatMap((metadata) => Object.keys(metadata.devDependencies ?? {})),
38
+ ]);
39
+ const workspace = Boolean(packageMetadata?.workspaces) || await exists(path.join(project.path, "pnpm-workspace.yaml"));
40
+ return {
41
+ project,
42
+ generatedAt: new Date().toISOString(),
43
+ gitStatus,
44
+ commit,
45
+ description: cleanDescription(packageMetadata?.description) ||
46
+ extractReadmeDescription(readme) ||
47
+ "暂未发现项目描述。",
48
+ packageManager: await detectPackageManager(project.path, packageMetadata?.packageManager),
49
+ workspace,
50
+ fileCount: indexedFiles.files.length,
51
+ indexTruncated: indexedFiles.truncated,
52
+ languages: detectLanguages(indexedFiles.files),
53
+ stack: detectStack(dependencyNames),
54
+ topDirectories: detectTopDirectories(indexedFiles.files),
55
+ scripts: Object.keys(packageMetadata?.scripts ?? {}).slice(0, 8),
56
+ };
57
+ }
58
+ async function readPackageMetadata(projectPath) {
59
+ return readPackageMetadataFile(path.join(projectPath, "package.json"));
60
+ }
61
+ async function readWorkspacePackageMetadata(projectPath, files) {
62
+ const manifests = files
63
+ .filter((file) => file !== "package.json" && file.endsWith("/package.json"))
64
+ .slice(0, 20);
65
+ const values = await Promise.all(manifests.map((manifest) => readPackageMetadataFile(path.join(projectPath, manifest))));
66
+ return values.filter((value) => value !== null);
67
+ }
68
+ async function readPackageMetadataFile(file) {
69
+ try {
70
+ if ((await stat(file)).size > MAX_METADATA_BYTES)
71
+ return null;
72
+ const value = JSON.parse(await readFile(file, "utf8"));
73
+ return isRecord(value) ? value : null;
74
+ }
75
+ catch {
76
+ return null;
77
+ }
78
+ }
79
+ async function readReadme(projectPath) {
80
+ try {
81
+ const entries = await readdir(projectPath);
82
+ const name = entries.find((entry) => /^readme(?:\.|$)/i.test(entry));
83
+ if (!name)
84
+ return "";
85
+ const file = path.join(projectPath, name);
86
+ if ((await stat(file)).size > MAX_METADATA_BYTES)
87
+ return "";
88
+ return await readFile(file, "utf8");
89
+ }
90
+ catch {
91
+ return "";
92
+ }
93
+ }
94
+ async function readGitCommit(projectPath) {
95
+ try {
96
+ const { stdout } = await execFileAsync("git", ["-C", projectPath, "rev-parse", "--short=10", "HEAD"], gitOptions(128 * 1024));
97
+ return stdout.trim() || null;
98
+ }
99
+ catch {
100
+ return null;
101
+ }
102
+ }
103
+ async function readGitFiles(projectPath) {
104
+ try {
105
+ const { stdout } = await execFileAsync("git", ["-C", projectPath, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], gitOptions(MAX_INDEX_BYTES));
106
+ const all = stdout.split("\0").filter(Boolean);
107
+ return { files: all.slice(0, MAX_FILES), truncated: all.length > MAX_FILES };
108
+ }
109
+ catch {
110
+ return readDirectoryFiles(projectPath);
111
+ }
112
+ }
113
+ async function readDirectoryFiles(projectPath) {
114
+ const files = [];
115
+ const pending = [projectPath];
116
+ while (pending.length > 0 && files.length < MAX_FILES) {
117
+ const directory = pending.shift();
118
+ if (!directory)
119
+ break;
120
+ let entries;
121
+ try {
122
+ entries = await readdir(directory, { withFileTypes: true });
123
+ }
124
+ catch {
125
+ continue;
126
+ }
127
+ for (const entry of entries) {
128
+ if (entry.isSymbolicLink())
129
+ continue;
130
+ const absolute = path.join(directory, entry.name);
131
+ if (entry.isDirectory()) {
132
+ if (!SKIP_DIRECTORIES.has(entry.name))
133
+ pending.push(absolute);
134
+ }
135
+ else if (entry.isFile()) {
136
+ files.push(path.relative(projectPath, absolute));
137
+ if (files.length >= MAX_FILES)
138
+ break;
139
+ }
140
+ }
141
+ }
142
+ return { files, truncated: pending.length > 0 };
143
+ }
144
+ function detectLanguages(files) {
145
+ const labels = {
146
+ ".ts": "TypeScript",
147
+ ".tsx": "TypeScript",
148
+ ".js": "JavaScript",
149
+ ".jsx": "JavaScript",
150
+ ".vue": "Vue",
151
+ ".py": "Python",
152
+ ".go": "Go",
153
+ ".rs": "Rust",
154
+ ".java": "Java",
155
+ ".kt": "Kotlin",
156
+ ".swift": "Swift",
157
+ ".php": "PHP",
158
+ ".rb": "Ruby",
159
+ ".css": "CSS",
160
+ ".scss": "SCSS",
161
+ ".less": "Less",
162
+ };
163
+ const counts = new Map();
164
+ for (const file of files) {
165
+ const label = labels[path.extname(file).toLocaleLowerCase()];
166
+ if (label)
167
+ counts.set(label, (counts.get(label) ?? 0) + 1);
168
+ }
169
+ return [...counts.entries()]
170
+ .sort((left, right) => right[1] - left[1])
171
+ .slice(0, 5)
172
+ .map(([label]) => label);
173
+ }
174
+ function detectStack(dependencies) {
175
+ const known = [
176
+ ["react", "React"],
177
+ ["next", "Next.js"],
178
+ ["vue", "Vue"],
179
+ ["nuxt", "Nuxt"],
180
+ ["@angular/core", "Angular"],
181
+ ["svelte", "Svelte"],
182
+ ["typescript", "TypeScript"],
183
+ ["vite", "Vite"],
184
+ ["@rspack/core", "Rspack"],
185
+ ["webpack", "Webpack"],
186
+ ["express", "Express"],
187
+ ["@nestjs/core", "NestJS"],
188
+ ["jest", "Jest"],
189
+ ["vitest", "Vitest"],
190
+ ["playwright", "Playwright"],
191
+ ["@playwright/test", "Playwright"],
192
+ ["zustand", "Zustand"],
193
+ ["redux", "Redux"],
194
+ ["@reduxjs/toolkit", "Redux Toolkit"],
195
+ ];
196
+ const seen = new Set();
197
+ const result = [];
198
+ for (const [dependency, label] of known) {
199
+ if (dependencies.has(dependency) && !seen.has(label)) {
200
+ seen.add(label);
201
+ result.push(label);
202
+ }
203
+ }
204
+ return result.slice(0, 8);
205
+ }
206
+ function detectTopDirectories(files) {
207
+ const counts = new Map();
208
+ for (const file of files) {
209
+ const [first, ...rest] = file.split("/");
210
+ if (!first || rest.length === 0 || SKIP_DIRECTORIES.has(first))
211
+ continue;
212
+ counts.set(first, (counts.get(first) ?? 0) + 1);
213
+ }
214
+ return [...counts.entries()]
215
+ .sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))
216
+ .slice(0, 6)
217
+ .map(([directory]) => directory);
218
+ }
219
+ async function detectPackageManager(projectPath, declared) {
220
+ if (declared?.trim())
221
+ return declared.trim().split("@")[0] || declared.trim();
222
+ if (await exists(path.join(projectPath, "pnpm-lock.yaml")))
223
+ return "pnpm";
224
+ if (await exists(path.join(projectPath, "yarn.lock")))
225
+ return "yarn";
226
+ if (await exists(path.join(projectPath, "bun.lockb")))
227
+ return "bun";
228
+ if (await exists(path.join(projectPath, "package-lock.json")))
229
+ return "npm";
230
+ return "未识别";
231
+ }
232
+ function extractReadmeDescription(source) {
233
+ if (!source.trim())
234
+ return "";
235
+ const withoutCode = source.replace(/```[\s\S]*?```/g, " ");
236
+ const paragraphs = withoutCode.split(/\n\s*\n/);
237
+ for (const paragraph of paragraphs) {
238
+ const cleaned = cleanDescription(paragraph
239
+ .replace(/^#{1,6}\s+.*$/gm, "")
240
+ .replace(/!\[[^\]]*]\([^)]*\)/g, "")
241
+ .replace(/\[[^\]]*]\([^)]*\)/g, (match) => match.replace(/^\[|]\([^)]*\)$/g, ""))
242
+ .replace(/<[^>]+>/g, "")
243
+ .replace(/^[-*>\s]+/gm, "")
244
+ .replace(/[`*_~]/g, ""));
245
+ if (cleaned.length >= 24)
246
+ return cleaned;
247
+ }
248
+ return "";
249
+ }
250
+ function cleanDescription(value) {
251
+ if (typeof value !== "string")
252
+ return "";
253
+ return value.replace(/\s+/g, " ").trim().slice(0, 240);
254
+ }
255
+ function gitOptions(maxBuffer) {
256
+ return {
257
+ timeout: 4_000,
258
+ maxBuffer,
259
+ encoding: "utf8",
260
+ env: {
261
+ ...process.env,
262
+ GIT_OPTIONAL_LOCKS: "0",
263
+ GIT_TERMINAL_PROMPT: "0",
264
+ },
265
+ };
266
+ }
267
+ async function exists(file) {
268
+ try {
269
+ await access(file);
270
+ return true;
271
+ }
272
+ catch {
273
+ return false;
274
+ }
275
+ }
276
+ function isRecord(value) {
277
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
278
+ }
@@ -0,0 +1,160 @@
1
+ import { lstat, readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ export const PROJECT_POLICY_FILE = ".feishu-codex-policy.json";
4
+ const MAX_POLICY_BYTES = 64 * 1024;
5
+ const EXTERNAL_ACTIONS = ["commit", "push", "pull_request", "deploy"];
6
+ export class ProjectPolicyError extends Error {
7
+ constructor(message) {
8
+ super(message);
9
+ this.name = "ProjectPolicyError";
10
+ }
11
+ }
12
+ export async function loadProjectPolicy(projectPath) {
13
+ const filePath = path.join(projectPath, PROJECT_POLICY_FILE);
14
+ let details;
15
+ try {
16
+ details = await lstat(filePath);
17
+ }
18
+ catch (error) {
19
+ if (error.code === "ENOENT")
20
+ return defaultProjectPolicy();
21
+ throw new ProjectPolicyError(`无法读取项目策略:${safeErrorMessage(error)}`);
22
+ }
23
+ if (details.isSymbolicLink() || !details.isFile()) {
24
+ throw new ProjectPolicyError(`${PROJECT_POLICY_FILE} 必须是项目根目录中的普通文件`);
25
+ }
26
+ if (details.size > MAX_POLICY_BYTES) {
27
+ throw new ProjectPolicyError(`${PROJECT_POLICY_FILE} 不能超过 64 KB`);
28
+ }
29
+ let parsed;
30
+ try {
31
+ parsed = JSON.parse(await readFile(filePath, "utf8"));
32
+ }
33
+ catch (error) {
34
+ throw new ProjectPolicyError(`项目策略不是有效 JSON:${safeErrorMessage(error)}`);
35
+ }
36
+ const record = requireRecord(parsed, "项目策略");
37
+ if (record.version !== 1)
38
+ throw new ProjectPolicyError("项目策略 version 必须为 1");
39
+ const sandbox = optionalRecord(record.sandbox, "sandbox");
40
+ const maximumSandbox = sandbox
41
+ ? optionalSandboxMode(sandbox.maximum, "sandbox.maximum")
42
+ : undefined;
43
+ const operations = optionalRecord(record.operations, "operations");
44
+ const allowedActions = operations
45
+ ? optionalActionList(operations.allow, "operations.allow")
46
+ : null;
47
+ const deniedActions = operations
48
+ ? optionalActionList(operations.deny, "operations.deny") ?? []
49
+ : [];
50
+ const configuredApprovals = operations
51
+ ? optionalActionList(operations.requireApproval, "operations.requireApproval") ?? []
52
+ : [];
53
+ return {
54
+ source: "repository",
55
+ filePath,
56
+ ...(maximumSandbox ? { maximumSandbox } : {}),
57
+ allowedActions,
58
+ deniedActions,
59
+ // External side effects always keep the bridge-level confirmation floor.
60
+ // Repository policy can add intent, but can never remove this list.
61
+ approvalActions: uniqueActions([...EXTERNAL_ACTIONS, ...configuredApprovals]),
62
+ };
63
+ }
64
+ export function defaultProjectPolicy() {
65
+ return {
66
+ source: "default",
67
+ allowedActions: null,
68
+ deniedActions: [],
69
+ approvalActions: [...EXTERNAL_ACTIONS],
70
+ };
71
+ }
72
+ export function applyProjectSandboxMaximum(requested, policy) {
73
+ if (!policy.maximumSandbox)
74
+ return requested;
75
+ return sandboxRank(requested) <= sandboxRank(policy.maximumSandbox)
76
+ ? requested
77
+ : policy.maximumSandbox;
78
+ }
79
+ export function decideProjectActions(policy, actions) {
80
+ const allowed = policy.allowedActions ? new Set(policy.allowedActions) : null;
81
+ const denied = new Set(policy.deniedActions);
82
+ const blockedActions = uniqueActions(actions.filter((action) => denied.has(action) || (allowed !== null && !allowed.has(action))));
83
+ const approvalFloor = new Set(policy.approvalActions);
84
+ return {
85
+ blockedActions,
86
+ approvalActions: uniqueActions(actions.filter((action) => !blockedActions.includes(action) && approvalFloor.has(action))),
87
+ };
88
+ }
89
+ export function projectPolicySummary(policy) {
90
+ if (policy.source === "default")
91
+ return "主机默认策略";
92
+ const restrictions = [
93
+ policy.maximumSandbox ? `权限最高 ${sandboxLabel(policy.maximumSandbox)}` : "",
94
+ policy.allowedActions ? `允许 ${policy.allowedActions.length} 类外部动作` : "",
95
+ policy.deniedActions.length > 0 ? `拒绝 ${policy.deniedActions.length} 类外部动作` : "",
96
+ ].filter(Boolean);
97
+ return restrictions.length > 0 ? `仓库策略 · ${restrictions.join(" · ")}` : "仓库策略 · 使用安全默认值";
98
+ }
99
+ function optionalActionList(value, label) {
100
+ if (value === undefined)
101
+ return null;
102
+ if (!Array.isArray(value))
103
+ throw new ProjectPolicyError(`${label} 必须是数组`);
104
+ if (value.length > EXTERNAL_ACTIONS.length) {
105
+ throw new ProjectPolicyError(`${label} 包含重复项或未知操作`);
106
+ }
107
+ const actions = [];
108
+ for (const entry of value) {
109
+ if (typeof entry !== "string" || !isExternalAction(entry)) {
110
+ throw new ProjectPolicyError(`${label} 仅支持:${EXTERNAL_ACTIONS.join(", ")}`);
111
+ }
112
+ if (actions.includes(entry))
113
+ throw new ProjectPolicyError(`${label} 不能包含重复项`);
114
+ actions.push(entry);
115
+ }
116
+ return actions;
117
+ }
118
+ function optionalSandboxMode(value, label) {
119
+ if (value === undefined)
120
+ return undefined;
121
+ if (value === "read-only" || value === "workspace-write" || value === "danger-full-access") {
122
+ return value;
123
+ }
124
+ throw new ProjectPolicyError(`${label} 仅支持 read-only、workspace-write 或 danger-full-access`);
125
+ }
126
+ function optionalRecord(value, label) {
127
+ if (value === undefined)
128
+ return undefined;
129
+ return requireRecord(value, label);
130
+ }
131
+ function requireRecord(value, label) {
132
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
133
+ throw new ProjectPolicyError(`${label} 必须是对象`);
134
+ }
135
+ return value;
136
+ }
137
+ function isExternalAction(value) {
138
+ return EXTERNAL_ACTIONS.includes(value);
139
+ }
140
+ function uniqueActions(actions) {
141
+ return [...new Set(actions)];
142
+ }
143
+ function sandboxRank(mode) {
144
+ if (mode === "read-only")
145
+ return 0;
146
+ if (mode === "workspace-write")
147
+ return 1;
148
+ return 2;
149
+ }
150
+ function sandboxLabel(mode) {
151
+ if (mode === "read-only")
152
+ return "只读";
153
+ if (mode === "workspace-write")
154
+ return "工作区写入";
155
+ return "完全访问";
156
+ }
157
+ function safeErrorMessage(error) {
158
+ const message = error instanceof Error ? error.message : String(error);
159
+ return message.replace(/[\r\n]+/g, " ").slice(0, 300);
160
+ }