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.
- package/.env.example +101 -0
- package/.feishu-codex-policy.example.json +11 -0
- package/.feishu-codex-runbooks.example.json +36 -0
- package/CHANGELOG.md +129 -0
- package/CODE_OF_CONDUCT.md +7 -0
- package/CONTRIBUTING.md +52 -0
- package/LICENSE +21 -0
- package/README.en.md +81 -0
- package/README.md +398 -0
- package/ROADMAP.md +40 -0
- package/SECURITY.md +53 -0
- package/dist/account-quota-card.js +233 -0
- package/dist/account-quota.js +125 -0
- package/dist/app-server-client.js +281 -0
- package/dist/card-session.js +166 -0
- package/dist/codex-events.js +1 -0
- package/dist/codex-runner.js +875 -0
- package/dist/config.js +198 -0
- package/dist/confirmation-card.js +135 -0
- package/dist/control-card.js +345 -0
- package/dist/conversation-turn-session.js +209 -0
- package/dist/data-maintenance.js +71 -0
- package/dist/device-card.js +460 -0
- package/dist/device-health.js +94 -0
- package/dist/diagnostics.js +253 -0
- package/dist/doctor.js +250 -0
- package/dist/fallback-card-session.js +37 -0
- package/dist/health-file.js +75 -0
- package/dist/index.js +4330 -0
- package/dist/lark-cli.js +558 -0
- package/dist/lark-retry.js +34 -0
- package/dist/maintenance.js +140 -0
- package/dist/model-capabilities.js +31 -0
- package/dist/onboarding-card.js +312 -0
- package/dist/permission-lease.js +22 -0
- package/dist/policy.js +506 -0
- package/dist/progress.js +267 -0
- package/dist/project-card.js +303 -0
- package/dist/project-overview-card.js +182 -0
- package/dist/project-overview.js +278 -0
- package/dist/project-policy.js +160 -0
- package/dist/project-registry.js +259 -0
- package/dist/project-status.js +45 -0
- package/dist/project-workspace.js +55 -0
- package/dist/quota-card.js +94 -0
- package/dist/recovery-policy.js +26 -0
- package/dist/redaction.js +67 -0
- package/dist/remote-ready.js +112 -0
- package/dist/response-card.js +139 -0
- package/dist/result-card.js +166 -0
- package/dist/review-card.js +452 -0
- package/dist/runbook-card.js +272 -0
- package/dist/runbooks.js +191 -0
- package/dist/runtime-card.js +337 -0
- package/dist/session-card.js +128 -0
- package/dist/session-naming.js +14 -0
- package/dist/smoke.js +28 -0
- package/dist/state-backup.js +302 -0
- package/dist/state-store.js +874 -0
- package/dist/task-card.js +640 -0
- package/dist/task-center-card.js +176 -0
- package/dist/task-failure.js +43 -0
- package/dist/task-intent.js +76 -0
- package/dist/task-queue.js +187 -0
- package/dist/task-reconciliation.js +80 -0
- package/dist/task-review.js +497 -0
- package/dist/team-card.js +275 -0
- package/dist/team-directory.js +54 -0
- package/dist/team-policy.js +93 -0
- package/dist/types.js +1 -0
- package/dist/version.js +9 -0
- package/dist/workspace-session.js +64 -0
- package/docs/ARCHITECTURE.md +54 -0
- package/docs/COMPATIBILITY.md +55 -0
- package/docs/CONFIGURATION.md +88 -0
- package/docs/DEMO.md +45 -0
- package/docs/GOOD_FIRST_ISSUES.md +23 -0
- package/docs/INSTALLATION.md +207 -0
- package/docs/OPEN_SOURCE_PRODUCT_PLAN.md +113 -0
- package/docs/PRODUCT_REQUIREMENTS_MAP.md +591 -0
- package/docs/RELEASE_CHECKLIST.md +65 -0
- package/docs/TEAM_DEPLOYMENT.md +35 -0
- package/docs/TROUBLESHOOTING.md +130 -0
- package/docs/V4_WORKSPACE_SESSION_FLOW.md +232 -0
- package/docs/requirements/D10_MAINTENANCE_AND_ECOSYSTEM.md +103 -0
- package/docs/requirements/D1_INSTALLATION_AND_FIRST_CONNECTION.md +479 -0
- package/docs/requirements/D2_DEVICE_AND_CONNECTIVITY.md +54 -0
- package/docs/requirements/D3_PROJECTS_AND_SESSIONS.md +107 -0
- package/docs/requirements/D4_REMOTE_TASK_EXECUTION.md +102 -0
- package/docs/requirements/D5_CODEX_NATIVE_INTERACTIONS.md +99 -0
- package/docs/requirements/D6_RESULTS_AND_CODE_REVIEW.md +100 -0
- package/docs/requirements/D7_SECURITY_GOVERNANCE.md +106 -0
- package/docs/requirements/D8_RELIABILITY_AND_RECOVERY.md +182 -0
- package/docs/requirements/D9_TEAM_COLLABORATION.md +129 -0
- package/package.json +76 -0
- package/scripts/capability-probe.mjs +113 -0
- package/scripts/cli.mjs +919 -0
- package/scripts/config-file.mjs +137 -0
- package/scripts/discovery-lib.mjs +78 -0
- package/scripts/install-card.mjs +37 -0
- package/scripts/install-detection.mjs +126 -0
- package/scripts/install-state.mjs +107 -0
- package/scripts/launchd.mjs +161 -0
- package/scripts/migrate-legacy.mjs +97 -0
- package/scripts/package-smoke.mjs +163 -0
- package/scripts/release-dist-tag.mjs +7 -0
- package/scripts/runbook-template.mjs +36 -0
- package/scripts/service-health.mjs +110 -0
- package/scripts/service.mjs +24 -0
- package/scripts/setup-lib.mjs +118 -0
- package/scripts/systemd.mjs +96 -0
- package/scripts/upgrade-lib.mjs +99 -0
- package/scripts/verify-release.mjs +37 -0
package/dist/progress.js
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { isTestCommand, summarizeCommandOutput, } from "./task-review.js";
|
|
2
|
+
import { completedActivity, inferTaskMode, runningActivity, taskModeOf, } from "./task-intent.js";
|
|
3
|
+
export function effectiveTestRuns(commandRuns = []) {
|
|
4
|
+
const latest = new Map();
|
|
5
|
+
for (const run of commandRuns) {
|
|
6
|
+
if (run.category !== "test")
|
|
7
|
+
continue;
|
|
8
|
+
latest.set(run.command.trim().replace(/\s+/g, " ").toLocaleLowerCase(), run);
|
|
9
|
+
}
|
|
10
|
+
return [...latest.values()];
|
|
11
|
+
}
|
|
12
|
+
export function testEvidenceState(commandRuns = []) {
|
|
13
|
+
const tests = effectiveTestRuns(commandRuns);
|
|
14
|
+
if (tests.length === 0)
|
|
15
|
+
return "missing";
|
|
16
|
+
if (tests.some((run) => run.status === "failed"))
|
|
17
|
+
return "failed";
|
|
18
|
+
if (tests.some((run) => run.status === "running"))
|
|
19
|
+
return "running";
|
|
20
|
+
return "passed";
|
|
21
|
+
}
|
|
22
|
+
export function updateTaskCollaboration(progress, context, note) {
|
|
23
|
+
return {
|
|
24
|
+
...progress,
|
|
25
|
+
...context,
|
|
26
|
+
...(note ? { actionNote: note } : {}),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const MAX_LOGS = 8;
|
|
30
|
+
const MAX_COMMAND_RUNS = 24;
|
|
31
|
+
export function createTaskProgress(taskId, prompt, queuePosition, projectLabel, now = Date.now(), permissionLabel = "工作区写入", context = {}) {
|
|
32
|
+
const taskMode = context.taskMode ?? inferTaskMode(prompt);
|
|
33
|
+
return {
|
|
34
|
+
taskId,
|
|
35
|
+
prompt,
|
|
36
|
+
projectLabel,
|
|
37
|
+
permissionLabel,
|
|
38
|
+
queuePosition,
|
|
39
|
+
phase: "queued",
|
|
40
|
+
activity: queuePosition > 1 ? `等待队列,第 ${queuePosition} 位` : "等待开始",
|
|
41
|
+
logs: [],
|
|
42
|
+
changedFiles: [],
|
|
43
|
+
partialResponse: "",
|
|
44
|
+
finalResponse: "",
|
|
45
|
+
error: "",
|
|
46
|
+
actionNote: "",
|
|
47
|
+
createdAt: now,
|
|
48
|
+
startedAt: null,
|
|
49
|
+
finishedAt: null,
|
|
50
|
+
usage: null,
|
|
51
|
+
threadId: "",
|
|
52
|
+
...context,
|
|
53
|
+
taskMode,
|
|
54
|
+
steerCount: 0,
|
|
55
|
+
commandRuns: [],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function startTask(progress, now = Date.now(), reviewBaseline, permissionLabel) {
|
|
59
|
+
const mode = taskModeOf(progress);
|
|
60
|
+
return appendLog({
|
|
61
|
+
...progress,
|
|
62
|
+
phase: "running",
|
|
63
|
+
activity: runningActivity(mode),
|
|
64
|
+
startedAt: progress.startedAt ?? now,
|
|
65
|
+
finishedAt: null,
|
|
66
|
+
actionNote: "",
|
|
67
|
+
...(reviewBaseline ? { reviewBaseline } : {}),
|
|
68
|
+
...(permissionLabel ? { permissionLabel } : {}),
|
|
69
|
+
}, runningActivity(mode));
|
|
70
|
+
}
|
|
71
|
+
export function recoverTask(progress, queuePosition, note = "服务重启后已恢复任务") {
|
|
72
|
+
return appendLog({
|
|
73
|
+
...progress,
|
|
74
|
+
phase: "queued",
|
|
75
|
+
queuePosition,
|
|
76
|
+
activity: "服务重启后等待恢复",
|
|
77
|
+
error: "",
|
|
78
|
+
actionNote: note,
|
|
79
|
+
finishedAt: null,
|
|
80
|
+
}, "已从可靠队列恢复");
|
|
81
|
+
}
|
|
82
|
+
export function updateQueuePosition(progress, queuePosition) {
|
|
83
|
+
if (progress.phase !== "queued" || progress.queuePosition === queuePosition)
|
|
84
|
+
return progress;
|
|
85
|
+
return {
|
|
86
|
+
...progress,
|
|
87
|
+
queuePosition,
|
|
88
|
+
activity: queuePosition > 1 ? `等待队列,第 ${queuePosition} 位` : "等待开始",
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
export function noteSteer(progress) {
|
|
92
|
+
const steerCount = (progress.steerCount ?? 0) + 1;
|
|
93
|
+
return {
|
|
94
|
+
...progress,
|
|
95
|
+
steerCount,
|
|
96
|
+
actionNote: `已接收 ${steerCount} 条补充要求,并加入当前 Codex turn。`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
export function applyCodexEvent(progress, event) {
|
|
100
|
+
switch (event.type) {
|
|
101
|
+
case "thread.started":
|
|
102
|
+
return appendLog({ ...progress, activity: "已连接 Codex 会话", threadId: event.thread_id }, "已连接 Codex 会话");
|
|
103
|
+
case "turn.started":
|
|
104
|
+
return appendLog({ ...progress, activity: runningActivity(taskModeOf(progress)) }, runningActivity(taskModeOf(progress)));
|
|
105
|
+
case "turn.completed":
|
|
106
|
+
return { ...progress, activity: "正在整理结果", usage: event.usage };
|
|
107
|
+
case "turn.failed":
|
|
108
|
+
return appendLog({ ...progress, activity: "执行失败", error: event.error.message }, `执行失败:${event.error.message}`);
|
|
109
|
+
case "error":
|
|
110
|
+
return appendLog({ ...progress, activity: "连接异常", error: event.message }, `连接异常:${event.message}`);
|
|
111
|
+
case "item.started":
|
|
112
|
+
case "item.updated":
|
|
113
|
+
case "item.completed":
|
|
114
|
+
return applyItem(progress, event.item, event.type);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export function succeedTask(progress, finalResponse, usage, threadId, now = Date.now()) {
|
|
118
|
+
const mode = taskModeOf(progress);
|
|
119
|
+
return appendLog({
|
|
120
|
+
...progress,
|
|
121
|
+
phase: "succeeded",
|
|
122
|
+
activity: completedActivity(mode),
|
|
123
|
+
finalResponse,
|
|
124
|
+
partialResponse: finalResponse || progress.partialResponse,
|
|
125
|
+
usage,
|
|
126
|
+
threadId,
|
|
127
|
+
finishedAt: now,
|
|
128
|
+
}, completedActivity(mode));
|
|
129
|
+
}
|
|
130
|
+
export function failTask(progress, error, now = Date.now()) {
|
|
131
|
+
return appendLog({
|
|
132
|
+
...progress,
|
|
133
|
+
phase: "failed",
|
|
134
|
+
activity: "任务失败",
|
|
135
|
+
error,
|
|
136
|
+
finishedAt: now,
|
|
137
|
+
}, `任务失败:${error}`);
|
|
138
|
+
}
|
|
139
|
+
export function cancelTask(progress, reason, now = Date.now()) {
|
|
140
|
+
return appendLog({
|
|
141
|
+
...progress,
|
|
142
|
+
phase: "cancelled",
|
|
143
|
+
activity: "任务已取消",
|
|
144
|
+
error: reason,
|
|
145
|
+
finishedAt: now,
|
|
146
|
+
}, `任务已取消:${reason}`);
|
|
147
|
+
}
|
|
148
|
+
export function interruptTask(progress, reason, now = Date.now()) {
|
|
149
|
+
return appendLog({
|
|
150
|
+
...progress,
|
|
151
|
+
phase: "interrupted",
|
|
152
|
+
activity: "任务被服务中断",
|
|
153
|
+
error: reason,
|
|
154
|
+
finishedAt: now,
|
|
155
|
+
}, `任务被服务中断:${reason}`);
|
|
156
|
+
}
|
|
157
|
+
export function noteTask(progress, note) {
|
|
158
|
+
return { ...progress, actionNote: note };
|
|
159
|
+
}
|
|
160
|
+
export function attachTaskReview(progress, review) {
|
|
161
|
+
return { ...progress, review };
|
|
162
|
+
}
|
|
163
|
+
function applyItem(progress, item, eventType) {
|
|
164
|
+
const completed = eventType === "item.completed";
|
|
165
|
+
switch (item.type) {
|
|
166
|
+
case "agent_message":
|
|
167
|
+
return {
|
|
168
|
+
...progress,
|
|
169
|
+
activity: "Codex 正在回复",
|
|
170
|
+
partialResponse: item.text,
|
|
171
|
+
};
|
|
172
|
+
case "reasoning":
|
|
173
|
+
return { ...progress, activity: "Codex 正在推理" };
|
|
174
|
+
case "command_execution": {
|
|
175
|
+
const command = shorten(item.command, 110);
|
|
176
|
+
if (eventType === "item.started") {
|
|
177
|
+
return appendLog({
|
|
178
|
+
...progress,
|
|
179
|
+
activity: `执行命令:${command}`,
|
|
180
|
+
commandRuns: upsertCommandRun(progress.commandRuns ?? [], {
|
|
181
|
+
id: item.id,
|
|
182
|
+
command: item.command,
|
|
183
|
+
category: isTestCommand(item.command) ? "test" : "command",
|
|
184
|
+
status: "running",
|
|
185
|
+
}),
|
|
186
|
+
}, `运行:${command}`);
|
|
187
|
+
}
|
|
188
|
+
if (completed) {
|
|
189
|
+
const ok = item.status === "completed" && (item.exit_code ?? 0) === 0;
|
|
190
|
+
return appendLog({
|
|
191
|
+
...progress,
|
|
192
|
+
activity: ok ? "命令执行完成" : "命令执行失败",
|
|
193
|
+
commandRuns: upsertCommandRun(progress.commandRuns ?? [], {
|
|
194
|
+
id: item.id,
|
|
195
|
+
command: item.command,
|
|
196
|
+
category: isTestCommand(item.command) ? "test" : "command",
|
|
197
|
+
status: ok ? "passed" : "failed",
|
|
198
|
+
...(item.exit_code === undefined ? {} : { exitCode: item.exit_code }),
|
|
199
|
+
...(isTestCommand(item.command) && item.aggregated_output
|
|
200
|
+
? { outputSummary: summarizeCommandOutput(item.aggregated_output) }
|
|
201
|
+
: {}),
|
|
202
|
+
}),
|
|
203
|
+
}, `${ok ? "✓" : "✗"} 命令${ok ? "完成" : "失败"}${item.exit_code === undefined ? "" : `(退出码 ${item.exit_code})`}`);
|
|
204
|
+
}
|
|
205
|
+
return { ...progress, activity: `执行命令:${command}` };
|
|
206
|
+
}
|
|
207
|
+
case "file_change": {
|
|
208
|
+
if (!completed)
|
|
209
|
+
return { ...progress, activity: "正在修改文件" };
|
|
210
|
+
const paths = item.changes.map((change) => change.path).slice(0, 4);
|
|
211
|
+
const extra = item.changes.length > paths.length ? ` 等 ${item.changes.length} 个文件` : "";
|
|
212
|
+
return appendLog({
|
|
213
|
+
...progress,
|
|
214
|
+
activity: item.status === "completed" ? "文件修改完成" : "文件修改失败",
|
|
215
|
+
changedFiles: item.status === "completed"
|
|
216
|
+
? mergeChangedFiles(progress.changedFiles, item.changes)
|
|
217
|
+
: progress.changedFiles,
|
|
218
|
+
}, `${item.status === "completed" ? "✓" : "✗"} ${paths.join("、")}${extra}`);
|
|
219
|
+
}
|
|
220
|
+
case "mcp_tool_call": {
|
|
221
|
+
const label = `${item.server}/${item.tool}`;
|
|
222
|
+
if (eventType === "item.started") {
|
|
223
|
+
return appendLog({ ...progress, activity: `调用工具:${label}` }, `调用工具:${label}`);
|
|
224
|
+
}
|
|
225
|
+
if (completed) {
|
|
226
|
+
const ok = item.status === "completed";
|
|
227
|
+
return appendLog({ ...progress, activity: ok ? "工具调用完成" : "工具调用失败" }, `${ok ? "✓" : "✗"} 工具 ${label}`);
|
|
228
|
+
}
|
|
229
|
+
return { ...progress, activity: `调用工具:${label}` };
|
|
230
|
+
}
|
|
231
|
+
case "web_search":
|
|
232
|
+
return eventType === "item.started"
|
|
233
|
+
? appendLog({ ...progress, activity: `搜索资料:${shorten(item.query, 80)}` }, `搜索:${shorten(item.query, 80)}`)
|
|
234
|
+
: progress;
|
|
235
|
+
case "todo_list": {
|
|
236
|
+
const completedCount = item.items.filter((todo) => todo.completed).length;
|
|
237
|
+
return {
|
|
238
|
+
...progress,
|
|
239
|
+
activity: `执行计划 ${completedCount}/${item.items.length}`,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
case "error":
|
|
243
|
+
return appendLog({ ...progress, activity: "步骤异常", error: item.message }, `步骤异常:${item.message}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function upsertCommandRun(current, incoming) {
|
|
247
|
+
const next = current.filter((run) => run.id !== incoming.id);
|
|
248
|
+
next.push(incoming);
|
|
249
|
+
return next.slice(-MAX_COMMAND_RUNS);
|
|
250
|
+
}
|
|
251
|
+
function mergeChangedFiles(current, incoming) {
|
|
252
|
+
const merged = new Map(current.map((change) => [change.path, change]));
|
|
253
|
+
for (const change of incoming)
|
|
254
|
+
merged.set(change.path, change);
|
|
255
|
+
return [...merged.values()].slice(-40);
|
|
256
|
+
}
|
|
257
|
+
function appendLog(progress, line) {
|
|
258
|
+
const clean = shorten(line.replace(/\s+/g, " ").trim(), 180);
|
|
259
|
+
if (!clean || progress.logs.at(-1) === clean)
|
|
260
|
+
return progress;
|
|
261
|
+
return { ...progress, logs: [...progress.logs, clean].slice(-MAX_LOGS) };
|
|
262
|
+
}
|
|
263
|
+
function shorten(value, max) {
|
|
264
|
+
if (value.length <= max)
|
|
265
|
+
return value;
|
|
266
|
+
return `${value.slice(0, Math.max(0, max - 1))}…`;
|
|
267
|
+
}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
export function renderProjectCard(projects, current, feedback = "", context = {}) {
|
|
2
|
+
const gitCount = projects.filter((project) => project.isGitRepository).length;
|
|
3
|
+
const folderCount = projects.length - gitCount;
|
|
4
|
+
return {
|
|
5
|
+
schema: "2.0",
|
|
6
|
+
config: {
|
|
7
|
+
update_multi: true,
|
|
8
|
+
width_mode: "default",
|
|
9
|
+
enable_forward: false,
|
|
10
|
+
summary: { content: `Codex 项目工作台 · 当前 ${safePlainText(current.name)}` },
|
|
11
|
+
style: {
|
|
12
|
+
text_size: {
|
|
13
|
+
project_name: { default: "heading-4", pc: "heading-4", mobile: "normal" },
|
|
14
|
+
caption: { default: "notation", pc: "notation", mobile: "notation" },
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
header: {
|
|
19
|
+
title: { tag: "plain_text", content: "Codex 项目工作台" },
|
|
20
|
+
subtitle: {
|
|
21
|
+
tag: "plain_text",
|
|
22
|
+
content: "选择项目后,后续任务会在对应目录中执行",
|
|
23
|
+
},
|
|
24
|
+
template: "blue",
|
|
25
|
+
icon: { tag: "standard_icon", token: "myai_colorful" },
|
|
26
|
+
text_tag_list: [
|
|
27
|
+
{
|
|
28
|
+
tag: "text_tag",
|
|
29
|
+
text: { tag: "plain_text", content: `${projects.length} 个项目` },
|
|
30
|
+
color: "blue",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
tag: "text_tag",
|
|
34
|
+
text: { tag: "plain_text", content: "自动同步" },
|
|
35
|
+
color: "violet",
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
body: {
|
|
40
|
+
direction: "vertical",
|
|
41
|
+
padding: "12px 12px 20px 12px",
|
|
42
|
+
vertical_spacing: "12px",
|
|
43
|
+
elements: [
|
|
44
|
+
currentProjectBlock(current, feedback, context),
|
|
45
|
+
projectFavoriteAction(current, context),
|
|
46
|
+
projectSelector(projects, current, context),
|
|
47
|
+
projectStats(projects.length, gitCount, folderCount),
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function currentProjectBlock(current, feedback, context) {
|
|
53
|
+
const favorite = context.favoritePaths?.includes(current.path) ?? false;
|
|
54
|
+
const elements = [
|
|
55
|
+
{
|
|
56
|
+
tag: "markdown",
|
|
57
|
+
element_id: "current_label",
|
|
58
|
+
text_size: "caption",
|
|
59
|
+
content: "<font color='blue'>当前项目</font>",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
tag: "markdown",
|
|
63
|
+
element_id: "current_name",
|
|
64
|
+
text_size: "project_name",
|
|
65
|
+
content: `**<font color='blue'>${safeMarkdown(current.name)}</font>** ${projectTag(current)}${favorite ? " <text_tag color='yellow'>已收藏</text_tag>" : ""}`,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
tag: "markdown",
|
|
69
|
+
element_id: "current_path",
|
|
70
|
+
text_size: "caption",
|
|
71
|
+
content: `<font color='grey'>${safeMarkdown(current.displayPath)}</font>`,
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
if (current.isGitRepository) {
|
|
75
|
+
elements.push({
|
|
76
|
+
tag: "markdown",
|
|
77
|
+
element_id: "current_git_status",
|
|
78
|
+
text_size: "caption",
|
|
79
|
+
content: `<font color='grey'>${safeMarkdown(gitStatusLabel(context.gitStatus))}</font>`,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
if (context.policyLabel) {
|
|
83
|
+
elements.push({
|
|
84
|
+
tag: "markdown",
|
|
85
|
+
element_id: "current_policy",
|
|
86
|
+
text_size: "caption",
|
|
87
|
+
content: `<font color='grey'>安全策略:${safeMarkdown(context.policyLabel)}</font>`,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
if (feedback) {
|
|
91
|
+
elements.push({
|
|
92
|
+
tag: "markdown",
|
|
93
|
+
element_id: "switch_feedback",
|
|
94
|
+
text_size: "caption",
|
|
95
|
+
content: `<font color='blue'>${safeMarkdown(feedback)}</font>`,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
tag: "column_set",
|
|
100
|
+
element_id: "current_panel",
|
|
101
|
+
flex_mode: "none",
|
|
102
|
+
columns: [
|
|
103
|
+
{
|
|
104
|
+
tag: "column",
|
|
105
|
+
width: "weighted",
|
|
106
|
+
weight: 1,
|
|
107
|
+
background_style: "blue-50",
|
|
108
|
+
padding: "12px 12px 12px 12px",
|
|
109
|
+
vertical_spacing: "4px",
|
|
110
|
+
elements,
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function projectSelector(projects, current, context) {
|
|
116
|
+
const canSwitch = context.canSwitch ?? true;
|
|
117
|
+
const favorite = new Set(context.favoritePaths ?? []);
|
|
118
|
+
const recent = new Set(context.recentPaths ?? []);
|
|
119
|
+
const switchImpact = switchImpactLabel(context);
|
|
120
|
+
return {
|
|
121
|
+
tag: "column_set",
|
|
122
|
+
element_id: "selector_panel",
|
|
123
|
+
flex_mode: "none",
|
|
124
|
+
columns: [
|
|
125
|
+
{
|
|
126
|
+
tag: "column",
|
|
127
|
+
width: "weighted",
|
|
128
|
+
weight: 1,
|
|
129
|
+
background_style: "grey-50",
|
|
130
|
+
padding: "12px 12px 12px 12px",
|
|
131
|
+
vertical_spacing: "8px",
|
|
132
|
+
elements: canSwitch ? [
|
|
133
|
+
{
|
|
134
|
+
tag: "markdown",
|
|
135
|
+
element_id: "selector_title",
|
|
136
|
+
content: "**查找并切换项目**\n<font color='grey'>可以输入项目名或路径搜索;收藏和最近使用排在前面</font>",
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
tag: "select_static",
|
|
140
|
+
element_id: "project_select",
|
|
141
|
+
name: "project_path",
|
|
142
|
+
width: "fill",
|
|
143
|
+
placeholder: { tag: "plain_text", content: "选择一个项目" },
|
|
144
|
+
initial_option: current.path,
|
|
145
|
+
options: projects.slice(0, 100).map((project) => ({
|
|
146
|
+
text: {
|
|
147
|
+
tag: "plain_text",
|
|
148
|
+
content: projectOptionLabel(project, favorite, recent),
|
|
149
|
+
},
|
|
150
|
+
value: project.path,
|
|
151
|
+
})),
|
|
152
|
+
confirm: {
|
|
153
|
+
title: { tag: "plain_text", content: "确认切换项目?" },
|
|
154
|
+
text: {
|
|
155
|
+
tag: "plain_text",
|
|
156
|
+
content: switchImpact,
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
behaviors: [
|
|
160
|
+
{
|
|
161
|
+
type: "callback",
|
|
162
|
+
value: { bridge: "feishu-codex-v2", action: "select_project" },
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
tag: "markdown",
|
|
168
|
+
element_id: "selector_hint",
|
|
169
|
+
text_size: "caption",
|
|
170
|
+
content: `<font color='grey'>${safeMarkdown(switchImpact)}${projects.length > 100 ? `\n当前显示前 100 个;也可发送“切换 项目名或路径”搜索全部 ${projects.length} 个。` : ""}</font>`,
|
|
171
|
+
},
|
|
172
|
+
] : [
|
|
173
|
+
{
|
|
174
|
+
tag: "markdown",
|
|
175
|
+
element_id: "selector_readonly",
|
|
176
|
+
content: "**项目列表为只读**\n<font color='grey'>当前身份可以查看项目和收藏当前项目,但不能切换执行目录。</font>",
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function projectFavoriteAction(current, context) {
|
|
184
|
+
const favorite = context.favoritePaths?.includes(current.path) ?? false;
|
|
185
|
+
return {
|
|
186
|
+
tag: "column_set",
|
|
187
|
+
element_id: "project_fav_actions",
|
|
188
|
+
flex_mode: "none",
|
|
189
|
+
columns: [
|
|
190
|
+
{
|
|
191
|
+
tag: "column",
|
|
192
|
+
width: "weighted",
|
|
193
|
+
weight: 1,
|
|
194
|
+
elements: [
|
|
195
|
+
{
|
|
196
|
+
tag: "button",
|
|
197
|
+
element_id: "project_fav_toggle",
|
|
198
|
+
text: { tag: "plain_text", content: favorite ? "取消收藏当前项目" : "收藏当前项目" },
|
|
199
|
+
type: favorite ? "default" : "primary_filled",
|
|
200
|
+
width: "fill",
|
|
201
|
+
behaviors: [
|
|
202
|
+
{
|
|
203
|
+
type: "callback",
|
|
204
|
+
value: { bridge: "feishu-codex-v2", action: "toggle_project_favorite" },
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function projectStats(total, git, folders) {
|
|
214
|
+
return {
|
|
215
|
+
tag: "column_set",
|
|
216
|
+
element_id: "project_stats",
|
|
217
|
+
flex_mode: "none",
|
|
218
|
+
horizontal_spacing: "8px",
|
|
219
|
+
columns: [
|
|
220
|
+
metricColumn("stats_total", total, "全部项目"),
|
|
221
|
+
metricColumn("stats_git", git, "Git 仓库"),
|
|
222
|
+
metricColumn("stats_folder", folders, "普通文件夹"),
|
|
223
|
+
],
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function metricColumn(elementId, value, label) {
|
|
227
|
+
return {
|
|
228
|
+
tag: "column",
|
|
229
|
+
width: "weighted",
|
|
230
|
+
weight: 1,
|
|
231
|
+
background_style: "grey-50",
|
|
232
|
+
padding: "8px 4px 8px 4px",
|
|
233
|
+
vertical_spacing: "2px",
|
|
234
|
+
elements: [
|
|
235
|
+
{
|
|
236
|
+
tag: "markdown",
|
|
237
|
+
element_id: `${elementId}_value`,
|
|
238
|
+
text_align: "center",
|
|
239
|
+
content: `**${value}**`,
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
tag: "markdown",
|
|
243
|
+
element_id: `${elementId}_label`,
|
|
244
|
+
text_align: "center",
|
|
245
|
+
text_size: "caption",
|
|
246
|
+
content: `<font color='grey'>${label}</font>`,
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
function projectTag(project) {
|
|
252
|
+
return project.isGitRepository
|
|
253
|
+
? "<text_tag color='blue'>Git</text_tag>"
|
|
254
|
+
: "<text_tag color='neutral'>文件夹</text_tag>";
|
|
255
|
+
}
|
|
256
|
+
function projectType(project) {
|
|
257
|
+
return project.isGitRepository ? "Git" : "文件夹";
|
|
258
|
+
}
|
|
259
|
+
function projectOptionLabel(project, favorite, recent) {
|
|
260
|
+
const marker = favorite.has(project.path) ? "★" : recent.has(project.path) ? "最近" : "项目";
|
|
261
|
+
return truncatePlain(`${marker} ${safePlainText(project.name)} · ${projectType(project)} · ${safePlainText(project.displayPath)}`, 90);
|
|
262
|
+
}
|
|
263
|
+
function gitStatusLabel(status) {
|
|
264
|
+
if (!status)
|
|
265
|
+
return "Git 分支状态暂不可用";
|
|
266
|
+
const branch = status.detached ? `分离状态 ${status.branch}` : `分支 ${status.branch}`;
|
|
267
|
+
const worktree = status.dirty ? `${status.changedFiles} 个未提交变更` : "工作区干净";
|
|
268
|
+
const divergence = [
|
|
269
|
+
status.ahead > 0 ? `领先 ${status.ahead}` : "",
|
|
270
|
+
status.behind > 0 ? `落后 ${status.behind}` : "",
|
|
271
|
+
].filter(Boolean).join(" · ");
|
|
272
|
+
return `${branch} · ${worktree}${divergence ? ` · ${divergence}` : ""}`;
|
|
273
|
+
}
|
|
274
|
+
function switchImpactLabel(context) {
|
|
275
|
+
const active = context.activeTasks ?? 0;
|
|
276
|
+
const queued = context.queuedTasks ?? 0;
|
|
277
|
+
const taskImpact = active + queued > 0
|
|
278
|
+
? `会停止 ${active} 个运行任务和 ${queued} 个排队任务`
|
|
279
|
+
: "当前没有未完成任务";
|
|
280
|
+
const sessionImpact = context.hasSavedThread
|
|
281
|
+
? "并清空当前聊天保存的 Codex 上下文"
|
|
282
|
+
: "并从新会话开始";
|
|
283
|
+
return `${taskImpact},${sessionImpact}。`;
|
|
284
|
+
}
|
|
285
|
+
function safeMarkdown(value) {
|
|
286
|
+
return value
|
|
287
|
+
.replaceAll("&", "&")
|
|
288
|
+
.replaceAll("<", "<")
|
|
289
|
+
.replaceAll(">", ">")
|
|
290
|
+
.replaceAll("*", "*")
|
|
291
|
+
.replaceAll("_", "_")
|
|
292
|
+
.replaceAll("`", "`")
|
|
293
|
+
.replaceAll("[", "[")
|
|
294
|
+
.replaceAll("]", "]");
|
|
295
|
+
}
|
|
296
|
+
function safePlainText(value) {
|
|
297
|
+
return value.replaceAll("<", "<").replaceAll(">", ">");
|
|
298
|
+
}
|
|
299
|
+
function truncatePlain(value, maxLength) {
|
|
300
|
+
if (value.length <= maxLength)
|
|
301
|
+
return value;
|
|
302
|
+
return `${value.slice(0, Math.max(0, maxLength - 1))}…`;
|
|
303
|
+
}
|