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,140 @@
1
+ import { chmod, lstat, mkdir, open, readFile, readdir, rm, stat, writeFile, } from "node:fs/promises";
2
+ import path from "node:path";
3
+ export async function isSupportedImageFile(file) {
4
+ const handle = await open(file, "r");
5
+ try {
6
+ const header = Buffer.alloc(16);
7
+ const { bytesRead } = await handle.read(header, 0, header.length, 0);
8
+ const bytes = header.subarray(0, bytesRead);
9
+ const png = bytes.length >= 8 &&
10
+ bytes.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]));
11
+ const jpeg = bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff;
12
+ const gif = bytes.length >= 6 &&
13
+ (bytes.subarray(0, 6).toString("ascii") === "GIF87a" ||
14
+ bytes.subarray(0, 6).toString("ascii") === "GIF89a");
15
+ const webp = bytes.length >= 12 &&
16
+ bytes.subarray(0, 4).toString("ascii") === "RIFF" &&
17
+ bytes.subarray(8, 12).toString("ascii") === "WEBP";
18
+ return png || jpeg || gif || webp;
19
+ }
20
+ finally {
21
+ await handle.close();
22
+ }
23
+ }
24
+ export async function isUtf8TextFile(file) {
25
+ try {
26
+ const bytes = await readFile(file);
27
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
28
+ return !text.includes("\u0000");
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ }
34
+ export async function cleanupAttachmentCache(rootDirectory, olderThanMs, protectedPaths = [], now = Date.now()) {
35
+ const root = path.resolve(rootDirectory);
36
+ const protectedFiles = new Set([...protectedPaths].map((file) => path.resolve(file)));
37
+ const result = { removedFiles: 0, reclaimedBytes: 0 };
38
+ try {
39
+ const rootDetails = await lstat(root);
40
+ if (!rootDetails.isDirectory() || rootDetails.isSymbolicLink()) {
41
+ throw new Error(`Unsafe attachment cache directory: ${root}`);
42
+ }
43
+ }
44
+ catch (error) {
45
+ if (error.code === "ENOENT")
46
+ return result;
47
+ throw error;
48
+ }
49
+ await walk(root, true);
50
+ return result;
51
+ async function walk(directory, isRoot) {
52
+ let entries;
53
+ try {
54
+ entries = await readdir(directory, { withFileTypes: true });
55
+ }
56
+ catch (error) {
57
+ if (error.code === "ENOENT")
58
+ return;
59
+ throw error;
60
+ }
61
+ await chmod(directory, 0o700);
62
+ for (const entry of entries) {
63
+ const candidate = path.resolve(directory, entry.name);
64
+ if (!isWithin(root, candidate) || entry.isSymbolicLink())
65
+ continue;
66
+ if (entry.isDirectory()) {
67
+ await walk(candidate, false);
68
+ continue;
69
+ }
70
+ if (!entry.isFile() || protectedFiles.has(candidate))
71
+ continue;
72
+ let details;
73
+ try {
74
+ details = await lstat(candidate);
75
+ }
76
+ catch {
77
+ continue;
78
+ }
79
+ if (now - details.mtimeMs < olderThanMs)
80
+ continue;
81
+ await rm(candidate, { force: true });
82
+ result.removedFiles += 1;
83
+ result.reclaimedBytes += details.size;
84
+ }
85
+ if (!isRoot) {
86
+ try {
87
+ await rm(directory);
88
+ }
89
+ catch (error) {
90
+ if (!["ENOTEMPTY", "ENOENT"].includes(error.code ?? "")) {
91
+ throw error;
92
+ }
93
+ }
94
+ }
95
+ }
96
+ }
97
+ export async function trimRuntimeLogs(logDirectory, maxBytes) {
98
+ await mkdir(logDirectory, { recursive: true });
99
+ const directoryDetails = await lstat(logDirectory);
100
+ if (!directoryDetails.isDirectory() || directoryDetails.isSymbolicLink()) {
101
+ throw new Error(`Unsafe log directory: ${logDirectory}`);
102
+ }
103
+ await chmod(logDirectory, 0o700);
104
+ let trimmed = 0;
105
+ for (const name of ["bridge.log", "bridge.error.log"]) {
106
+ const file = path.join(logDirectory, name);
107
+ let details;
108
+ try {
109
+ details = await stat(file);
110
+ }
111
+ catch (error) {
112
+ if (error.code === "ENOENT")
113
+ continue;
114
+ throw error;
115
+ }
116
+ if (!details.isFile())
117
+ continue;
118
+ await chmod(file, 0o600);
119
+ if (details.size <= maxBytes)
120
+ continue;
121
+ const keepBytes = Math.min(Math.max(64 * 1024, Math.floor(maxBytes / 4)), 1024 * 1024);
122
+ const handle = await open(file, "r");
123
+ const tail = Buffer.alloc(Math.min(keepBytes, details.size));
124
+ try {
125
+ await handle.read(tail, 0, tail.length, details.size - tail.length);
126
+ }
127
+ finally {
128
+ await handle.close();
129
+ }
130
+ await writeFile(`${file}.1`, tail, { mode: 0o600 });
131
+ await writeFile(file, "", { mode: 0o600 });
132
+ await chmod(file, 0o600);
133
+ trimmed += 1;
134
+ }
135
+ return trimmed;
136
+ }
137
+ function isWithin(root, candidate) {
138
+ const relative = path.relative(root, candidate);
139
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
140
+ }
@@ -0,0 +1,31 @@
1
+ export function resolveModelPreference(conversationModel, configuredModel) {
2
+ if (conversationModel === "__default__")
3
+ return undefined;
4
+ return conversationModel ?? configuredModel;
5
+ }
6
+ export function compatibleModelSettings(requested, models) {
7
+ const settings = { ...requested };
8
+ const notices = [];
9
+ if (models.length === 0)
10
+ return { settings, notices };
11
+ let effectiveModel = settings.model
12
+ ? models.find((model) => model.model === settings.model || model.id === settings.model)
13
+ : models.find((model) => model.isDefault) ?? models[0];
14
+ if (settings.model && !effectiveModel) {
15
+ notices.push(`模型“${settings.model}”当前不可用,已回退到 Codex 默认模型`);
16
+ delete settings.model;
17
+ effectiveModel = models.find((model) => model.isDefault) ?? models[0];
18
+ }
19
+ if (settings.reasoningEffort &&
20
+ effectiveModel &&
21
+ effectiveModel.supportedReasoningEfforts.length > 0 &&
22
+ !effectiveModel.supportedReasoningEfforts.includes(settings.reasoningEffort)) {
23
+ notices.push(`当前模型不支持推理强度“${settings.reasoningEffort}”,已回退到“${effectiveModel.defaultReasoningEffort}”`);
24
+ settings.reasoningEffort = effectiveModel.defaultReasoningEffort;
25
+ }
26
+ return {
27
+ settings,
28
+ ...(effectiveModel ? { effectiveModel } : {}),
29
+ notices,
30
+ };
31
+ }
@@ -0,0 +1,312 @@
1
+ export function renderOnboardingCard(snapshot) {
2
+ const completed = snapshot.state.status === "completed";
3
+ const dismissed = snapshot.state.status === "dismissed";
4
+ return {
5
+ schema: "2.0",
6
+ config: {
7
+ update_multi: true,
8
+ width_mode: "default",
9
+ enable_forward: false,
10
+ summary: {
11
+ content: summary(snapshot),
12
+ },
13
+ },
14
+ header: {
15
+ title: {
16
+ tag: "plain_text",
17
+ content: headerTitle(snapshot),
18
+ },
19
+ subtitle: {
20
+ tag: "plain_text",
21
+ content: headerSubtitle(snapshot),
22
+ },
23
+ template: dismissed
24
+ ? "grey"
25
+ : !snapshot.deviceOnline || !snapshot.projectAvailable
26
+ ? "orange"
27
+ : completed
28
+ ? "green"
29
+ : "turquoise",
30
+ icon: { tag: "standard_icon", token: "myai_colorful" },
31
+ text_tag_list: [
32
+ {
33
+ tag: "text_tag",
34
+ text: { tag: "plain_text", content: roleLabel(snapshot.role) },
35
+ color: snapshot.role === "admin" ? "violet" : snapshot.role === "viewer" ? "neutral" : "blue",
36
+ },
37
+ {
38
+ tag: "text_tag",
39
+ text: {
40
+ tag: "plain_text",
41
+ content: snapshot.deviceOnline ? "设备在线" : "设备重连中",
42
+ },
43
+ color: snapshot.deviceOnline ? "green" : "orange",
44
+ },
45
+ ],
46
+ },
47
+ body: {
48
+ direction: "vertical",
49
+ padding: "12px 12px 18px 12px",
50
+ vertical_spacing: "12px",
51
+ elements: [
52
+ ...(snapshot.feedback ? [feedbackPanel(snapshot.feedback)] : []),
53
+ contentPanel(snapshot),
54
+ ...actionRows(snapshot),
55
+ {
56
+ tag: "markdown",
57
+ element_id: "onboard_safety",
58
+ text_size: "notation",
59
+ text_align: "center",
60
+ content: `<font color='grey'>${footerText(snapshot)}</font>`,
61
+ },
62
+ ],
63
+ },
64
+ };
65
+ }
66
+ function feedbackPanel(feedback) {
67
+ return {
68
+ tag: "column_set",
69
+ element_id: "onboard_feedback",
70
+ flex_mode: "none",
71
+ columns: [
72
+ {
73
+ tag: "column",
74
+ width: "weighted",
75
+ weight: 1,
76
+ background_style: "blue-50",
77
+ padding: "10px 12px 10px 12px",
78
+ elements: [
79
+ {
80
+ tag: "markdown",
81
+ element_id: "onboard_notice",
82
+ content: `<font color='blue'>${safe(feedback)}</font>`,
83
+ },
84
+ ],
85
+ },
86
+ ],
87
+ };
88
+ }
89
+ function contentPanel(snapshot) {
90
+ return {
91
+ tag: "column_set",
92
+ element_id: "onboard_content",
93
+ flex_mode: "none",
94
+ columns: [
95
+ {
96
+ tag: "column",
97
+ width: "weighted",
98
+ weight: 1,
99
+ background_style: "grey-50",
100
+ padding: "12px 12px 12px 12px",
101
+ elements: [
102
+ {
103
+ tag: "markdown",
104
+ element_id: "onboard_copy",
105
+ content: content(snapshot),
106
+ },
107
+ ],
108
+ },
109
+ ],
110
+ };
111
+ }
112
+ function content(snapshot) {
113
+ if (snapshot.state.status === "dismissed") {
114
+ return [
115
+ "**引导已关闭,不影响继续使用。**",
116
+ "",
117
+ "你仍然可以正常使用机器人;任何时候发送 `新手引导`、`/onboarding` 或 `/start` 都能重新开始。",
118
+ ].join("\n");
119
+ }
120
+ if (!snapshot.deviceOnline) {
121
+ return [
122
+ "**本地 Codex 正在重新连接。**",
123
+ "",
124
+ "先打开连接状态查看原因;恢复后回到聊天框,重新发送刚才的话即可。",
125
+ ].join("\n");
126
+ }
127
+ if (!snapshot.projectAvailable) {
128
+ return [
129
+ "**先选择一个要使用的项目。**",
130
+ "",
131
+ "Codex 只会读取或修改已授权项目;不需要输入本机路径,直接从项目列表中选择即可。",
132
+ ].join("\n");
133
+ }
134
+ if (snapshot.role === "viewer") {
135
+ return [
136
+ "**你现在是只读成员。**",
137
+ "",
138
+ `- 当前项目:${safe(snapshot.projectName)}`,
139
+ "- 可以查看项目、历史会话和任务状态",
140
+ "- 不能启动 Codex 或修改本地文件",
141
+ "",
142
+ "需要执行工作时,请联系管理员调整你的角色。",
143
+ ].join("\n");
144
+ }
145
+ return [
146
+ snapshot.state.status === "completed"
147
+ ? "**可以开始了,直接在聊天框里说你想做什么。**"
148
+ : "**直接在聊天框里说你想做什么。**",
149
+ "",
150
+ "你可以这样说:",
151
+ "- `这个项目是做什么的?`",
152
+ "- `找出登录慢的原因,先不要修改文件。`",
153
+ snapshot.canWrite
154
+ ? "- `运行测试,修复失败用例并说明改了什么。`"
155
+ : "- `检查现有测试并说明风险,不要修改文件。`",
156
+ "",
157
+ "继续同一件事,直接回复上一条消息;新事情就发一条新消息。",
158
+ "",
159
+ snapshot.groupChatEnabled
160
+ ? "**团队协作**:一个项目建一个群,每件新事发一条新消息。"
161
+ : "**团队协作**:一个项目建一个群;团队群可以以后再开,不影响现在使用。",
162
+ ...(snapshot.canWrite
163
+ ? []
164
+ : ["", "当前是只读模式;需要写文件时,可打开设置调整权限。"]),
165
+ ].join("\n");
166
+ }
167
+ function actionRows(snapshot) {
168
+ if (snapshot.state.status === "dismissed") {
169
+ return [
170
+ actionRow("onboard_primary", [
171
+ button("重新打开引导", "onboarding_restart", "primary"),
172
+ ]),
173
+ ];
174
+ }
175
+ if (!snapshot.deviceOnline) {
176
+ return [
177
+ actionRow("onboard_primary", [
178
+ button("查看连接状态", "onboarding_device", "primary"),
179
+ ]),
180
+ actionRow("onboard_secondary", [
181
+ button("稍后再说", "onboarding_dismiss", "default"),
182
+ ]),
183
+ ];
184
+ }
185
+ if (!snapshot.projectAvailable) {
186
+ return [
187
+ actionRow("onboard_primary", [
188
+ button("选择项目", "onboarding_projects", "primary"),
189
+ ]),
190
+ actionRow("onboard_secondary", [
191
+ button("稍后再说", "onboarding_dismiss", "default"),
192
+ ]),
193
+ ];
194
+ }
195
+ if (snapshot.state.status === "completed") {
196
+ return [
197
+ actionRow("onboard_actions", [
198
+ button("打开控制台", "onboarding_device", "primary"),
199
+ button("重新查看引导", "onboarding_restart", "default"),
200
+ ]),
201
+ ];
202
+ }
203
+ if (snapshot.role === "viewer") {
204
+ return [
205
+ actionRow("onboard_primary", [
206
+ button("我知道了", "onboarding_finish", "primary"),
207
+ ]),
208
+ actionRow("onboard_secondary", [
209
+ button("查看项目", "onboarding_projects", "default"),
210
+ button("任务中心", "onboarding_tasks", "default"),
211
+ ]),
212
+ ];
213
+ }
214
+ return [
215
+ actionRow("onboard_primary", [
216
+ button("知道了,开始使用", "onboarding_finish", "primary"),
217
+ ]),
218
+ actionRow("onboard_secondary", [
219
+ button("换项目", "onboarding_projects", "default"),
220
+ button("调整设置", "onboarding_settings", "default"),
221
+ ]),
222
+ ];
223
+ }
224
+ function actionRow(elementId, actions) {
225
+ return {
226
+ tag: "column_set",
227
+ element_id: elementId,
228
+ flex_mode: "none",
229
+ horizontal_spacing: "8px",
230
+ columns: actions.map((action) => ({
231
+ tag: "column",
232
+ width: "weighted",
233
+ weight: 1,
234
+ elements: [action],
235
+ })),
236
+ };
237
+ }
238
+ function summary(snapshot) {
239
+ if (snapshot.state.status === "dismissed")
240
+ return "Codex 新手引导已关闭";
241
+ if (!snapshot.deviceOnline)
242
+ return "本地 Codex 正在重新连接";
243
+ if (!snapshot.projectAvailable)
244
+ return "Codex 等待选择项目";
245
+ if (snapshot.state.status === "completed")
246
+ return "Codex 已准备好";
247
+ return `Codex 已连接 · ${safe(snapshot.projectName)}`;
248
+ }
249
+ function headerTitle(snapshot) {
250
+ if (snapshot.state.status === "dismissed")
251
+ return "引导已关闭";
252
+ if (!snapshot.deviceOnline)
253
+ return "正在连接本地 Codex";
254
+ if (!snapshot.projectAvailable)
255
+ return "先选择一个项目";
256
+ if (snapshot.state.status === "completed")
257
+ return "可以开始了";
258
+ return snapshot.role === "viewer" ? "欢迎使用 Codex" : "开始使用 Codex";
259
+ }
260
+ function headerSubtitle(snapshot) {
261
+ if (snapshot.state.status === "dismissed")
262
+ return "需要时发送“新手引导”即可重新打开";
263
+ if (!snapshot.deviceOnline)
264
+ return "查看连接状态,恢复后即可继续";
265
+ if (!snapshot.projectAvailable)
266
+ return "只需从已授权项目中选择";
267
+ if (snapshot.role === "viewer")
268
+ return `${safe(snapshot.projectName)} · 只读`;
269
+ return `${safe(snapshot.projectName)} · ${safe(snapshot.sandboxLabel)}`;
270
+ }
271
+ function footerText(snapshot) {
272
+ if (!snapshot.deviceOnline)
273
+ return "本地连接恢复后才能执行工作。";
274
+ if (!snapshot.projectAvailable)
275
+ return "这里只会显示管理员已经授权的项目。";
276
+ if (snapshot.role === "viewer")
277
+ return "只读成员不会触发本地执行或文件修改。";
278
+ return "任务只会在当前项目中运行;提交、推送、部署和 PR 仍需单独确认。";
279
+ }
280
+ function button(text, action, type) {
281
+ return {
282
+ tag: "button",
283
+ element_id: action.replace("onboarding_", "ob_").slice(0, 20),
284
+ text: { tag: "plain_text", content: text },
285
+ type: type === "primary" ? "primary_filled" : "default",
286
+ width: "fill",
287
+ behaviors: [
288
+ {
289
+ type: "callback",
290
+ value: { bridge: "feishu-codex-v5", action },
291
+ },
292
+ ],
293
+ };
294
+ }
295
+ function roleLabel(role) {
296
+ if (role === "admin")
297
+ return "管理员";
298
+ if (role === "operator")
299
+ return "操作者";
300
+ return "只读成员";
301
+ }
302
+ function safe(value) {
303
+ return value
304
+ .replaceAll("&", "&")
305
+ .replaceAll("<", "<")
306
+ .replaceAll(">", ">")
307
+ .replaceAll("*", "*")
308
+ .replaceAll("_", "_")
309
+ .replaceAll("`", "`")
310
+ .replaceAll("[", "[")
311
+ .replaceAll("]", "]");
312
+ }
@@ -0,0 +1,22 @@
1
+ export const FULL_ACCESS_ONCE_TTL_MINUTES = 30;
2
+ export const FULL_ACCESS_TIMED_TTL_MINUTES = 30;
3
+ export const FULL_ACCESS_SESSION_TTL_MINUTES = 60;
4
+ export function persistentSandboxMode(requested, maximum) {
5
+ const safeMaximum = maximum === "danger-full-access" ? "workspace-write" : maximum;
6
+ if (!requested || requested === "danger-full-access")
7
+ return safeMaximum;
8
+ if (requested === "workspace-write" && safeMaximum === "read-only")
9
+ return "read-only";
10
+ return requested;
11
+ }
12
+ export function permissionLeaseIsActive(lease, now = Date.now()) {
13
+ return Date.parse(lease.expiresAt) > now && lease.remainingUses > 0;
14
+ }
15
+ export function permissionLeaseLabel(lease, now = Date.now()) {
16
+ const minutes = Math.max(1, Math.ceil((Date.parse(lease.expiresAt) - now) / 60_000));
17
+ if (lease.scope === "next-task")
18
+ return `下一任务 · ${minutes} 分钟内有效`;
19
+ if (lease.scope === "session")
20
+ return `当前会话 · 最长剩余 ${minutes} 分钟`;
21
+ return `限时完全访问 · 剩余约 ${minutes} 分钟`;
22
+ }