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
package/dist/config.js ADDED
@@ -0,0 +1,198 @@
1
+ import { existsSync, realpathSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import path from "node:path";
4
+ function csv(value) {
5
+ return (value ?? "")
6
+ .split(",")
7
+ .map((item) => item.trim())
8
+ .filter(Boolean);
9
+ }
10
+ function bool(value, fallback) {
11
+ if (value === undefined || value.trim() === "")
12
+ return fallback;
13
+ if (["1", "true", "yes", "on"].includes(value.toLowerCase()))
14
+ return true;
15
+ if (["0", "false", "no", "off"].includes(value.toLowerCase()))
16
+ return false;
17
+ throw new Error(`Invalid boolean value: ${value}`);
18
+ }
19
+ function positiveInt(value, fallback, name) {
20
+ if (value === undefined || value.trim() === "")
21
+ return fallback;
22
+ const parsed = Number.parseInt(value, 10);
23
+ if (!Number.isSafeInteger(parsed) || parsed <= 0) {
24
+ throw new Error(`${name} must be a positive integer`);
25
+ }
26
+ return parsed;
27
+ }
28
+ function optionalEnum(value, allowed, name) {
29
+ if (!value)
30
+ return undefined;
31
+ if (!allowed.includes(value)) {
32
+ throw new Error(`${name} must be one of: ${allowed.join(", ")}`);
33
+ }
34
+ return value;
35
+ }
36
+ function safeInstanceId(value) {
37
+ const normalized = (value?.trim() || "default")
38
+ .toLocaleLowerCase()
39
+ .replace(/[^a-z0-9_-]+/g, "-")
40
+ .replace(/^-+|-+$/g, "")
41
+ .slice(0, 40);
42
+ if (!normalized)
43
+ throw new Error("BRIDGE_INSTANCE_ID must contain letters or numbers");
44
+ return normalized;
45
+ }
46
+ function projectAcl(value) {
47
+ if (!value?.trim())
48
+ return new Map();
49
+ let parsed;
50
+ try {
51
+ parsed = JSON.parse(value);
52
+ }
53
+ catch {
54
+ throw new Error("FEISHU_PROJECT_ACL_JSON must be valid JSON");
55
+ }
56
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
57
+ throw new Error("FEISHU_PROJECT_ACL_JSON must be an object of project selectors to open_id arrays");
58
+ }
59
+ const result = new Map();
60
+ for (const [selector, actors] of Object.entries(parsed)) {
61
+ if (!Array.isArray(actors) || actors.some((actor) => typeof actor !== "string" || !actor.trim())) {
62
+ throw new Error(`FEISHU_PROJECT_ACL_JSON entry ${selector} must be an array of open_ids`);
63
+ }
64
+ result.set(selector.trim(), new Set(actors.map((actor) => actor.trim())));
65
+ }
66
+ return result;
67
+ }
68
+ function memberLabels(value) {
69
+ if (!value?.trim())
70
+ return new Map();
71
+ let parsed;
72
+ try {
73
+ parsed = JSON.parse(value);
74
+ }
75
+ catch {
76
+ throw new Error("FEISHU_MEMBER_LABELS_JSON must be valid JSON");
77
+ }
78
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
79
+ throw new Error("FEISHU_MEMBER_LABELS_JSON must be an object of open_ids to display names");
80
+ }
81
+ const result = new Map();
82
+ for (const [memberId, label] of Object.entries(parsed)) {
83
+ if (!memberId.trim() || typeof label !== "string" || !label.trim()) {
84
+ throw new Error("FEISHU_MEMBER_LABELS_JSON keys and display names must be non-empty strings");
85
+ }
86
+ if (label.trim().length > 60) {
87
+ throw new Error(`FEISHU_MEMBER_LABELS_JSON display name for ${memberId} is too long`);
88
+ }
89
+ result.set(memberId.trim(), label.trim());
90
+ }
91
+ return result;
92
+ }
93
+ export function loadConfig(env = process.env, projectDir = process.cwd()) {
94
+ const configVersion = positiveInt(env.BRIDGE_CONFIG_VERSION, 1, "BRIDGE_CONFIG_VERSION");
95
+ if (configVersion !== 1) {
96
+ throw new Error(`BRIDGE_CONFIG_VERSION ${configVersion} is newer than this bridge supports (1)`);
97
+ }
98
+ const senderIds = csv(env.ALLOWED_FEISHU_OPEN_IDS);
99
+ if (senderIds.length === 0) {
100
+ throw new Error("ALLOWED_FEISHU_OPEN_IDS is required. Refusing to start an unrestricted remote coding bot.");
101
+ }
102
+ const requestedWorkdir = path.resolve(env.CODEX_WORKDIR || projectDir);
103
+ if (!existsSync(requestedWorkdir)) {
104
+ throw new Error(`CODEX_WORKDIR does not exist: ${requestedWorkdir}`);
105
+ }
106
+ const workdir = realpathSync(requestedWorkdir);
107
+ const configuredRoots = csv(env.CODEX_PROJECT_ROOTS);
108
+ const projectRoots = [
109
+ ...new Set((configuredRoots.length > 0 ? configuredRoots : [workdir]).map((root) => {
110
+ const resolved = path.resolve(root);
111
+ if (!existsSync(resolved)) {
112
+ throw new Error(`CODEX_PROJECT_ROOTS entry does not exist: ${resolved}`);
113
+ }
114
+ return realpathSync(resolved);
115
+ })),
116
+ ];
117
+ if (!projectRoots.some((root) => isWithin(root, workdir)))
118
+ projectRoots.push(workdir);
119
+ const sandboxMode = optionalEnum(env.CODEX_SANDBOX_MODE || "workspace-write", ["read-only", "workspace-write", "danger-full-access"], "CODEX_SANDBOX_MODE");
120
+ if (!sandboxMode)
121
+ throw new Error("CODEX_SANDBOX_MODE is required");
122
+ const operatorSandboxMode = optionalEnum(env.CODEX_OPERATOR_SANDBOX_MODE ||
123
+ (sandboxMode === "danger-full-access" ? "workspace-write" : sandboxMode), ["read-only", "workspace-write", "danger-full-access"], "CODEX_OPERATOR_SANDBOX_MODE");
124
+ if (!operatorSandboxMode)
125
+ throw new Error("CODEX_OPERATOR_SANDBOX_MODE is required");
126
+ const sandboxRank = { "read-only": 0, "workspace-write": 1, "danger-full-access": 2 };
127
+ if (sandboxRank[operatorSandboxMode] > sandboxRank[sandboxMode]) {
128
+ throw new Error("CODEX_OPERATOR_SANDBOX_MODE cannot exceed CODEX_SANDBOX_MODE");
129
+ }
130
+ const webSearchMode = optionalEnum(env.CODEX_WEB_SEARCH_MODE || "disabled", ["disabled", "cached", "live"], "CODEX_WEB_SEARCH_MODE");
131
+ if (!webSearchMode)
132
+ throw new Error("CODEX_WEB_SEARCH_MODE is required");
133
+ const codexHome = path.resolve(env.CODEX_HOME?.trim() || path.join(homedir(), ".codex"));
134
+ const dataDir = path.resolve(env.BRIDGE_DATA_DIR?.trim() || path.join(projectDir, "var"));
135
+ const instanceId = safeInstanceId(env.BRIDGE_INSTANCE_ID);
136
+ const adminIds = csv(env.FEISHU_ADMIN_OPEN_IDS);
137
+ const bundledLarkCli = path.join(projectDir, "node_modules", ".bin", "lark-cli");
138
+ const reasoningEffort = optionalEnum(env.CODEX_REASONING_EFFORT, ["minimal", "low", "medium", "high", "xhigh", "ultra"], "CODEX_REASONING_EFFORT");
139
+ const config = {
140
+ configVersion: 1,
141
+ projectDir: realpathSync(projectDir),
142
+ dataDir,
143
+ instanceId,
144
+ serviceLabel: `com.feishu-codex-bridge.${instanceId}`,
145
+ workdir,
146
+ projectRoots,
147
+ projectScanDepth: positiveInt(env.CODEX_PROJECT_SCAN_DEPTH, 8, "CODEX_PROJECT_SCAN_DEPTH"),
148
+ maxProjects: positiveInt(env.MAX_CODEX_PROJECTS, 200, "MAX_CODEX_PROJECTS"),
149
+ syncSavedProjects: bool(env.CODEX_SYNC_SAVED_PROJECTS, false),
150
+ codexProjectStateFile: path.resolve(env.CODEX_PROJECT_STATE_FILE?.trim() || path.join(codexHome, ".codex-global-state.json")),
151
+ databaseFile: env.BRIDGE_DATABASE_FILE
152
+ ? path.resolve(projectDir, env.BRIDGE_DATABASE_FILE)
153
+ : path.join(dataDir, "state.sqlite"),
154
+ stateFile: env.BRIDGE_STATE_FILE
155
+ ? path.resolve(projectDir, env.BRIDGE_STATE_FILE)
156
+ : path.join(dataDir, "state.json"),
157
+ larkCliPath: env.LARK_CLI_PATH || (existsSync(bundledLarkCli) ? bundledLarkCli : "lark-cli"),
158
+ allowedSenderIds: new Set(senderIds),
159
+ adminSenderIds: new Set(adminIds.length > 0 ? adminIds : senderIds),
160
+ viewerSenderIds: new Set(csv(env.FEISHU_VIEWER_OPEN_IDS)),
161
+ allowedChatIds: new Set(csv(env.ALLOWED_FEISHU_CHAT_IDS)),
162
+ groupSessionScope: optionalEnum(env.FEISHU_GROUP_SESSION_SCOPE || "member", ["member", "chat"], "FEISHU_GROUP_SESSION_SCOPE") ?? "member",
163
+ projectAcl: projectAcl(env.FEISHU_PROJECT_ACL_JSON),
164
+ memberLabels: memberLabels(env.FEISHU_MEMBER_LABELS_JSON),
165
+ botMentionNames: csv(env.FEISHU_BOT_MENTION_NAMES),
166
+ sandboxMode,
167
+ operatorSandboxMode,
168
+ skipGitRepoCheck: bool(env.CODEX_SKIP_GIT_REPO_CHECK, false),
169
+ networkAccessEnabled: bool(env.CODEX_NETWORK_ACCESS, false),
170
+ webSearchMode,
171
+ multiAgentEnabled: bool(env.CODEX_MULTI_AGENT_ENABLED, false),
172
+ maxReplyChars: positiveInt(env.MAX_REPLY_CHARS, 12_000, "MAX_REPLY_CHARS"),
173
+ codexTimeoutMs: positiveInt(env.CODEX_TIMEOUT_MS, 20 * 60_000, "CODEX_TIMEOUT_MS"),
174
+ maxSeenEvents: positiveInt(env.MAX_SEEN_EVENTS, 2_000, "MAX_SEEN_EVENTS"),
175
+ maxConcurrentTasks: positiveInt(env.MAX_CONCURRENT_TASKS, 2, "MAX_CONCURRENT_TASKS"),
176
+ maxQueuedPerConversation: positiveInt(env.MAX_QUEUED_PER_CONVERSATION, 5, "MAX_QUEUED_PER_CONVERSATION"),
177
+ maxAttachmentBytes: positiveInt(env.MAX_ATTACHMENT_BYTES, 20 * 1024 * 1024, "MAX_ATTACHMENT_BYTES"),
178
+ maxTextAttachmentBytes: positiveInt(env.MAX_TEXT_ATTACHMENT_BYTES, 128 * 1024, "MAX_TEXT_ATTACHMENT_BYTES"),
179
+ attachmentRetentionHours: positiveInt(env.ATTACHMENT_RETENTION_HOURS, 72, "ATTACHMENT_RETENTION_HOURS"),
180
+ confirmationTtlMinutes: positiveInt(env.EXTERNAL_CONFIRMATION_TTL_MINUTES, 10, "EXTERNAL_CONFIRMATION_TTL_MINUTES"),
181
+ maxLogBytes: positiveInt(env.MAX_LOG_BYTES, 10 * 1024 * 1024, "MAX_LOG_BYTES"),
182
+ codexAllowedEnvVars: csv(env.CODEX_ALLOWED_ENV_VARS),
183
+ logMessageContent: bool(env.LOG_MESSAGE_CONTENT, false),
184
+ completionNotifications: bool(env.FEISHU_COMPLETION_NOTIFICATIONS, false),
185
+ autoOnboarding: bool(env.FEISHU_AUTO_ONBOARDING, true),
186
+ };
187
+ if (env.CODEX_MODEL?.trim())
188
+ config.model = env.CODEX_MODEL.trim();
189
+ if (env.CODEX_CLI_PATH?.trim())
190
+ config.codexCliPath = path.resolve(env.CODEX_CLI_PATH.trim());
191
+ if (reasoningEffort)
192
+ config.reasoningEffort = reasoningEffort;
193
+ return config;
194
+ }
195
+ function isWithin(root, candidate) {
196
+ const relative = path.relative(root, candidate);
197
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
198
+ }
@@ -0,0 +1,135 @@
1
+ import { redactSensitiveText } from "./redaction.js";
2
+ const ACTION_LABELS = {
3
+ commit: "提交代码",
4
+ push: "推送远端",
5
+ deploy: "发布 / 部署",
6
+ pull_request: "创建 / 合并 PR",
7
+ };
8
+ export function renderExternalConfirmationCard(requestId, prompt, projectLabel, actions, state = "pending", ttlMinutes = 10) {
9
+ const labels = actions.map((action) => ACTION_LABELS[action]).join("、");
10
+ const elements = [
11
+ {
12
+ tag: "column_set",
13
+ element_id: "risk_summary",
14
+ flex_mode: "none",
15
+ columns: [
16
+ {
17
+ tag: "column",
18
+ width: "weighted",
19
+ weight: 1,
20
+ background_style: state === "pending" ? "orange-50" : "grey-50",
21
+ padding: "12px 12px 12px 12px",
22
+ vertical_spacing: "4px",
23
+ elements: [
24
+ {
25
+ tag: "markdown",
26
+ content: `**外部动作**\n${safeText(labels)}\n\n<font color='grey'>项目:${safeText(projectLabel)}</font>`,
27
+ },
28
+ ],
29
+ },
30
+ ],
31
+ },
32
+ {
33
+ tag: "markdown",
34
+ element_id: "request_text",
35
+ content: `**原始任务**\n${safeText(truncate(prompt, 1_500))}`,
36
+ },
37
+ ];
38
+ if (state === "pending") {
39
+ elements.push({
40
+ tag: "column_set",
41
+ element_id: "confirm_actions",
42
+ flex_mode: "none",
43
+ horizontal_spacing: "8px",
44
+ columns: [
45
+ buttonColumn("approve_btn", "确认并执行", "danger_filled", requestId, "approve_external", {
46
+ title: "确认执行外部动作?",
47
+ text: `Codex 将获得本次 ${labels} 的明确授权。`,
48
+ }),
49
+ buttonColumn("reject_btn", "取消", "default", requestId, "reject_external"),
50
+ ],
51
+ });
52
+ }
53
+ else {
54
+ elements.push({
55
+ tag: "markdown",
56
+ element_id: "decision",
57
+ text_align: "center",
58
+ content: state === "approved"
59
+ ? "<font color='green'>已确认,任务已进入执行队列</font>"
60
+ : state === "expired"
61
+ ? "<font color='grey'>确认已过期,请重新发送任务</font>"
62
+ : "<font color='grey'>已取消,本次任务未执行</font>",
63
+ });
64
+ }
65
+ return {
66
+ schema: "2.0",
67
+ config: {
68
+ update_multi: true,
69
+ width_mode: "default",
70
+ enable_forward: false,
71
+ summary: { content: `Codex 外部动作确认 · ${projectLabel}` },
72
+ },
73
+ header: {
74
+ title: { tag: "plain_text", content: "需要你的确认" },
75
+ subtitle: {
76
+ tag: "plain_text",
77
+ content: `外部动作不会被静默执行;确认 ${ttlMinutes} 分钟内有效`,
78
+ },
79
+ template: state === "pending" ? "orange" : state === "approved" ? "green" : "grey",
80
+ icon: { tag: "standard_icon", token: "warning_colorful" },
81
+ text_tag_list: [
82
+ {
83
+ tag: "text_tag",
84
+ text: { tag: "plain_text", content: "完全访问门禁" },
85
+ color: "orange",
86
+ },
87
+ ],
88
+ },
89
+ body: {
90
+ direction: "vertical",
91
+ padding: "12px 12px 20px 12px",
92
+ vertical_spacing: "12px",
93
+ elements,
94
+ },
95
+ };
96
+ }
97
+ function buttonColumn(elementId, label, type, requestId, action, confirm) {
98
+ const button = {
99
+ tag: "button",
100
+ element_id: elementId,
101
+ text: { tag: "plain_text", content: label },
102
+ type,
103
+ width: "fill",
104
+ behaviors: [
105
+ {
106
+ type: "callback",
107
+ value: { bridge: "feishu-codex-v3", action, request_id: requestId },
108
+ },
109
+ ],
110
+ };
111
+ if (confirm) {
112
+ button.confirm = {
113
+ title: { tag: "plain_text", content: confirm.title },
114
+ text: { tag: "plain_text", content: confirm.text },
115
+ };
116
+ }
117
+ return {
118
+ tag: "column",
119
+ width: "weighted",
120
+ weight: 1,
121
+ elements: [button],
122
+ };
123
+ }
124
+ function safeText(value) {
125
+ return redactSensitiveText(value)
126
+ .replaceAll("<at", "<at")
127
+ .replaceAll("</at>", "</at>")
128
+ .replaceAll("<person", "<person")
129
+ .replaceAll("</person>", "</person>");
130
+ }
131
+ function truncate(value, max) {
132
+ if (value.length <= max)
133
+ return value;
134
+ return `${value.slice(0, max - 1)}…`;
135
+ }
@@ -0,0 +1,345 @@
1
+ import { renderAccountQuotaSummary } from "./account-quota-card.js";
2
+ export function renderControlCenterCard(snapshot) {
3
+ const models = modelOptions(snapshot.models, snapshot.selectedModel);
4
+ const selected = snapshot.models.find((model) => model.model === snapshot.selectedModel) ??
5
+ snapshot.models.find((model) => model.isDefault);
6
+ const readOnly = snapshot.role === "viewer";
7
+ const catalogEditable = !readOnly && snapshot.modelCatalogAvailable;
8
+ const efforts = uniqueEfforts(selected?.supportedReasoningEfforts.length
9
+ ? selected.supportedReasoningEfforts
10
+ : ["minimal", "low", "medium", "high", "xhigh"]);
11
+ const selectedEffort = efforts.includes(snapshot.selectedEffort)
12
+ ? snapshot.selectedEffort
13
+ : selected?.defaultReasoningEffort ?? efforts[0] ?? snapshot.selectedEffort;
14
+ return {
15
+ schema: "2.0",
16
+ config: {
17
+ update_multi: true,
18
+ width_mode: "default",
19
+ enable_forward: false,
20
+ summary: { content: `Codex 控制中心 · ${safe(snapshot.projectName)}` },
21
+ },
22
+ header: {
23
+ title: { tag: "plain_text", content: "Codex 控制中心" },
24
+ subtitle: {
25
+ tag: "plain_text",
26
+ content: readOnly ? "只读查看当前会话设置" : "模型、推理与权限会在下一轮真实生效",
27
+ },
28
+ template: "turquoise",
29
+ icon: { tag: "standard_icon", token: "settings_colorful" },
30
+ text_tag_list: [
31
+ {
32
+ tag: "text_tag",
33
+ text: { tag: "plain_text", content: roleLabel(snapshot.role) },
34
+ color: snapshot.role === "admin" ? "violet" : "blue",
35
+ },
36
+ {
37
+ tag: "text_tag",
38
+ text: { tag: "plain_text", content: safe(snapshot.projectName).slice(0, 28) },
39
+ color: "turquoise",
40
+ },
41
+ ...(!snapshot.modelCatalogAvailable
42
+ ? [
43
+ {
44
+ tag: "text_tag",
45
+ text: { tag: "plain_text", content: "兼容模式" },
46
+ color: "neutral",
47
+ },
48
+ ]
49
+ : []),
50
+ ],
51
+ },
52
+ body: {
53
+ direction: "vertical",
54
+ padding: "12px 12px 18px 12px",
55
+ vertical_spacing: "12px",
56
+ elements: [
57
+ ...(snapshot.feedback ? [feedbackPanel(snapshot.feedback)] : []),
58
+ ...(snapshot.accountQuota
59
+ ? [renderAccountQuotaSummary(snapshot.accountQuota, "cq")]
60
+ : []),
61
+ selectorPanel("model_panel", "模型", readOnly
62
+ ? "由会话操作者管理"
63
+ : snapshot.modelCatalogAvailable
64
+ ? "选择 Codex 模型"
65
+ : "当前 Codex 未提供模型目录,保留已有配置", !catalogEditable
66
+ ? readOnlyValue("model_value", snapshot.selectedModel === "__default__"
67
+ ? `Codex 默认${selected ? ` · ${selected.displayName}` : ""}`
68
+ : selected?.displayName ?? snapshot.selectedModel)
69
+ : {
70
+ tag: "select_static",
71
+ element_id: "model_select",
72
+ name: "model",
73
+ width: "fill",
74
+ initial_option: snapshot.selectedModel,
75
+ options: models,
76
+ behaviors: [callback("select_model")],
77
+ }),
78
+ selectorPanel("effort_panel", "推理强度", snapshot.modelCatalogAvailable
79
+ ? effortDescription(selectedEffort)
80
+ : "模型能力未知,沿用当前配置", !catalogEditable
81
+ ? readOnlyValue("effort_value", reasoningEffortLabel(selectedEffort))
82
+ : {
83
+ tag: "select_static",
84
+ element_id: "effort_select",
85
+ name: "effort",
86
+ width: "fill",
87
+ initial_option: selectedEffort,
88
+ options: efforts.map((effort) => ({
89
+ text: { tag: "plain_text", content: reasoningEffortLabel(effort) },
90
+ value: effort,
91
+ })),
92
+ behaviors: [callback("select_effort")],
93
+ }),
94
+ selectorPanel("access_panel", "默认任务权限", `${sandboxDescription(snapshot.selectedSandbox)};长期设置最高为工作区写入`, readOnly
95
+ ? readOnlyValue("access_value", sandboxLabel(snapshot.selectedSandbox))
96
+ : {
97
+ tag: "select_static",
98
+ element_id: "access_select",
99
+ name: "sandbox",
100
+ width: "fill",
101
+ initial_option: snapshot.selectedSandbox,
102
+ options: snapshot.sandboxModes.map((mode) => ({
103
+ text: { tag: "plain_text", content: sandboxLabel(mode) },
104
+ value: mode,
105
+ })),
106
+ behaviors: [callback("select_sandbox")],
107
+ }),
108
+ ...(snapshot.fullAccessMaximum
109
+ ? [
110
+ fullAccessPanel(readOnly, snapshot.fullAccessLeaseLabel, snapshot.fullAccessSessionAvailable === true),
111
+ ]
112
+ : []),
113
+ actionGrid("control_actions", [
114
+ button("历史会话", "settings_sessions", "default"),
115
+ button("任务中心", "settings_tasks", "default"),
116
+ ...(readOnly ? [] : [button("新会话", "settings_new", "primary")]),
117
+ button("刷新", "settings_refresh", "default"),
118
+ ]),
119
+ {
120
+ tag: "markdown",
121
+ element_id: "control_safety",
122
+ text_size: "notation",
123
+ text_align: "center",
124
+ content: readOnly
125
+ ? "<font color='grey'>只读成员不能修改设置、创建会话或运行 Codex 任务。</font>"
126
+ : "<font color='grey'>完全访问只通过临时租约启用;提交、推送、部署和 PR 始终需要单独确认。</font>",
127
+ },
128
+ ],
129
+ },
130
+ };
131
+ }
132
+ function fullAccessPanel(readOnly, activeLease, sessionAvailable) {
133
+ const elements = [
134
+ {
135
+ tag: "markdown",
136
+ element_id: "full_access_title",
137
+ content: `**临时完全访问**\n<font color='grey'>${activeLease ? `当前:${safe(activeLease)};租约到期后自动回退` : "当前未启用;租约到期后自动回到默认任务权限"}</font>`,
138
+ },
139
+ ];
140
+ if (!readOnly) {
141
+ elements.push(actionGrid("lease_grant_actions", [
142
+ button("下一任务", "lease_full_once", "danger", {
143
+ title: "授权下一任务完全访问?",
144
+ text: "30 分钟内启动的下一项任务可执行本机命令,用完立即失效。",
145
+ }),
146
+ button("30 分钟", "lease_full_30m", "danger", {
147
+ title: "授权 30 分钟完全访问?",
148
+ text: "当前成员会话在 30 分钟内启动的任务都可使用完全访问。",
149
+ }),
150
+ ...(sessionAvailable
151
+ ? [
152
+ button("当前会话", "lease_full_session", "danger", {
153
+ title: "授权当前 Codex 会话?",
154
+ text: "仅绑定当前 thread,最长 60 分钟;切换或重置会话后立即失效。",
155
+ }),
156
+ ]
157
+ : []),
158
+ ]));
159
+ if (activeLease) {
160
+ elements.push(actionGrid("lease_revoke_actions", [
161
+ button("立即撤销", "lease_full_revoke", "default", {
162
+ title: "撤销临时完全访问?",
163
+ text: "不会中断已启动任务,但后续任务立即回到默认权限。",
164
+ }),
165
+ ]));
166
+ }
167
+ }
168
+ return {
169
+ tag: "column_set",
170
+ element_id: "full_access_panel",
171
+ flex_mode: "none",
172
+ columns: [
173
+ {
174
+ tag: "column",
175
+ width: "weighted",
176
+ weight: 1,
177
+ background_style: activeLease ? "orange-50" : "grey-50",
178
+ padding: "12px 12px 12px 12px",
179
+ vertical_spacing: "10px",
180
+ elements,
181
+ },
182
+ ],
183
+ };
184
+ }
185
+ function readOnlyValue(elementId, value) {
186
+ return {
187
+ tag: "markdown",
188
+ element_id: elementId,
189
+ content: `**${safe(value)}**`,
190
+ };
191
+ }
192
+ function selectorPanel(id, title, description, selector) {
193
+ return {
194
+ tag: "column_set",
195
+ element_id: id,
196
+ flex_mode: "none",
197
+ columns: [
198
+ {
199
+ tag: "column",
200
+ width: "weighted",
201
+ weight: 1,
202
+ background_style: "grey-50",
203
+ padding: "12px 12px 12px 12px",
204
+ vertical_spacing: "8px",
205
+ elements: [
206
+ {
207
+ tag: "markdown",
208
+ element_id: `${id}_title`,
209
+ content: `**${title}**\n<font color='grey'>${safe(description)}</font>`,
210
+ },
211
+ selector,
212
+ ],
213
+ },
214
+ ],
215
+ };
216
+ }
217
+ function feedbackPanel(feedback) {
218
+ return {
219
+ tag: "column_set",
220
+ element_id: "control_feedback",
221
+ flex_mode: "none",
222
+ columns: [
223
+ {
224
+ tag: "column",
225
+ width: "weighted",
226
+ weight: 1,
227
+ background_style: "green-50",
228
+ padding: "10px 12px 10px 12px",
229
+ elements: [
230
+ {
231
+ tag: "markdown",
232
+ element_id: "feedback_text",
233
+ content: `<font color='green'>${safe(feedback)}</font>`,
234
+ },
235
+ ],
236
+ },
237
+ ],
238
+ };
239
+ }
240
+ function modelOptions(models, selectedModel) {
241
+ const defaultModel = models.find((model) => model.isDefault);
242
+ const options = [
243
+ {
244
+ text: {
245
+ tag: "plain_text",
246
+ content: `Codex 默认${defaultModel ? ` · ${defaultModel.displayName}` : ""}`.slice(0, 80),
247
+ },
248
+ value: "__default__",
249
+ },
250
+ ...models.slice(0, 39).map((model) => ({
251
+ text: {
252
+ tag: "plain_text",
253
+ content: `${model.displayName}${model.isDefault ? " · 固定" : ""}`.slice(0, 80),
254
+ },
255
+ value: model.model,
256
+ })),
257
+ ];
258
+ if (!options.some((option) => option.value === selectedModel)) {
259
+ options.unshift({
260
+ text: { tag: "plain_text", content: `${selectedModel} · 当前配置` },
261
+ value: selectedModel,
262
+ });
263
+ }
264
+ return options;
265
+ }
266
+ function callback(action) {
267
+ return { type: "callback", value: { bridge: "feishu-codex-v5", action } };
268
+ }
269
+ function button(text, action, type, confirm) {
270
+ const result = {
271
+ tag: "button",
272
+ element_id: action.slice(0, 20),
273
+ text: { tag: "plain_text", content: text },
274
+ type: type === "primary" ? "primary_filled" : type === "danger" ? "danger_filled" : "default",
275
+ width: "fill",
276
+ behaviors: [callback(action)],
277
+ };
278
+ if (confirm) {
279
+ result.confirm = {
280
+ title: { tag: "plain_text", content: confirm.title },
281
+ text: { tag: "plain_text", content: confirm.text },
282
+ };
283
+ }
284
+ return result;
285
+ }
286
+ function actionGrid(id, actions) {
287
+ return {
288
+ tag: "column_set",
289
+ element_id: id,
290
+ flex_mode: "none",
291
+ horizontal_spacing: "8px",
292
+ columns: actions.map((action) => ({
293
+ tag: "column",
294
+ width: "weighted",
295
+ weight: 1,
296
+ elements: [action],
297
+ })),
298
+ };
299
+ }
300
+ function uniqueEfforts(values) {
301
+ return [...new Set(values)];
302
+ }
303
+ export function reasoningEffortLabel(value) {
304
+ return {
305
+ minimal: "最低",
306
+ low: "轻度",
307
+ medium: "中",
308
+ high: "高",
309
+ xhigh: "极高",
310
+ ultra: "最高",
311
+ }[value];
312
+ }
313
+ function effortDescription(value) {
314
+ return `当前 ${reasoningEffortLabel(value)};强度越高通常耗时越长`;
315
+ }
316
+ function sandboxLabel(value) {
317
+ if (value === "read-only")
318
+ return "只读分析";
319
+ if (value === "workspace-write")
320
+ return "工作区写入";
321
+ return "完全访问";
322
+ }
323
+ function sandboxDescription(value) {
324
+ if (value === "read-only")
325
+ return "只能读取和分析,不修改文件";
326
+ if (value === "workspace-write")
327
+ return "可修改当前项目,越界操作需要确认";
328
+ return "可执行本机命令;高风险外部动作仍需二次确认";
329
+ }
330
+ function roleLabel(role) {
331
+ if (role === "admin")
332
+ return "管理员";
333
+ if (role === "operator")
334
+ return "操作者";
335
+ return "只读成员";
336
+ }
337
+ function safe(value) {
338
+ return value
339
+ .replaceAll("&", "&")
340
+ .replaceAll("<", "<")
341
+ .replaceAll(">", ">")
342
+ .replaceAll("*", "*")
343
+ .replaceAll("_", "_")
344
+ .replaceAll("`", "`");
345
+ }