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
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { redactSensitiveText } from "./redaction.js";
|
|
2
|
+
export function renderRuntimeApprovalCard(requestId, request, projectLabel, state = "pending", ttlMinutes = 10) {
|
|
3
|
+
const pending = state === "pending";
|
|
4
|
+
const kindLabel = approvalKindLabel(request.kind);
|
|
5
|
+
const elements = [
|
|
6
|
+
{
|
|
7
|
+
tag: "column_set",
|
|
8
|
+
element_id: "approval_detail",
|
|
9
|
+
flex_mode: "none",
|
|
10
|
+
columns: [
|
|
11
|
+
{
|
|
12
|
+
tag: "column",
|
|
13
|
+
width: "weighted",
|
|
14
|
+
weight: 1,
|
|
15
|
+
background_style: pending ? "orange-50" : "grey-50",
|
|
16
|
+
padding: "12px 12px 12px 12px",
|
|
17
|
+
vertical_spacing: "6px",
|
|
18
|
+
elements: [
|
|
19
|
+
{
|
|
20
|
+
tag: "markdown",
|
|
21
|
+
content: `**${safe(request.title)}**\n${safe(truncate(request.detail, 2_000))}`,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
tag: "markdown",
|
|
25
|
+
text_size: "notation",
|
|
26
|
+
content: `<font color='grey'>类型:${kindLabel}\n项目:${safe(projectLabel)}${request.cwd ? `\n目录:${safe(request.cwd)}` : ""}${request.reason ? `\n原因:${safe(request.reason)}` : ""}\n范围:允许一次仅用于当前请求;本会话允许可能放行后续同类请求</font>`,
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
if (pending) {
|
|
34
|
+
elements.push({
|
|
35
|
+
tag: "column_set",
|
|
36
|
+
element_id: "approval_actions",
|
|
37
|
+
flex_mode: "none",
|
|
38
|
+
horizontal_spacing: "8px",
|
|
39
|
+
columns: [
|
|
40
|
+
actionButton("allow_once", "允许一次", "primary_filled", requestId, "approve_runtime_once"),
|
|
41
|
+
actionButton("allow_session", "本会话允许", "default", requestId, "approve_runtime_session", {
|
|
42
|
+
title: "本会话持续允许?",
|
|
43
|
+
text: "同类权限在当前 Codex 会话中可能不再询问。",
|
|
44
|
+
}),
|
|
45
|
+
actionButton("deny", "拒绝", "default", requestId, "reject_runtime"),
|
|
46
|
+
],
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
elements.push({
|
|
51
|
+
tag: "markdown",
|
|
52
|
+
element_id: "approval_result",
|
|
53
|
+
text_align: "center",
|
|
54
|
+
content: state === "accepted"
|
|
55
|
+
? "<font color='green'>已允许一次</font>"
|
|
56
|
+
: state === "session"
|
|
57
|
+
? "<font color='green'>已在本会话允许</font>"
|
|
58
|
+
: state === "expired"
|
|
59
|
+
? "<font color='grey'>请求已过期,Codex 未获得权限。发送“任务”查看最新任务。</font>"
|
|
60
|
+
: "<font color='grey'>已拒绝,Codex 未获得权限</font>",
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
schema: "2.0",
|
|
65
|
+
config: {
|
|
66
|
+
update_multi: true,
|
|
67
|
+
width_mode: "default",
|
|
68
|
+
enable_forward: false,
|
|
69
|
+
summary: { content: `Codex 运行时确认 · ${projectLabel}` },
|
|
70
|
+
},
|
|
71
|
+
header: {
|
|
72
|
+
title: { tag: "plain_text", content: "Codex 需要你的确认" },
|
|
73
|
+
subtitle: {
|
|
74
|
+
tag: "plain_text",
|
|
75
|
+
content: `这是本地 Codex 的实时权限请求;${ttlMinutes} 分钟内有效`,
|
|
76
|
+
},
|
|
77
|
+
template: pending ? "orange" : state === "accepted" || state === "session" ? "green" : "grey",
|
|
78
|
+
icon: { tag: "standard_icon", token: "warning_colorful" },
|
|
79
|
+
text_tag_list: [
|
|
80
|
+
{
|
|
81
|
+
tag: "text_tag",
|
|
82
|
+
text: { tag: "plain_text", content: "运行时权限" },
|
|
83
|
+
color: pending ? "orange" : "neutral",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
tag: "text_tag",
|
|
87
|
+
text: { tag: "plain_text", content: kindLabel },
|
|
88
|
+
color: "neutral",
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
body: {
|
|
93
|
+
direction: "vertical",
|
|
94
|
+
padding: "12px 12px 20px 12px",
|
|
95
|
+
vertical_spacing: "12px",
|
|
96
|
+
elements,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
export function renderRuntimeQuestionCard(requestId, question, position, state = "pending", answer = "", ttlMinutes = 10) {
|
|
101
|
+
const pending = state === "pending";
|
|
102
|
+
const answerAllowed = !question.isSecret;
|
|
103
|
+
const elements = [
|
|
104
|
+
{
|
|
105
|
+
tag: "column_set",
|
|
106
|
+
element_id: "question_detail",
|
|
107
|
+
flex_mode: "none",
|
|
108
|
+
columns: [
|
|
109
|
+
{
|
|
110
|
+
tag: "column",
|
|
111
|
+
width: "weighted",
|
|
112
|
+
weight: 1,
|
|
113
|
+
background_style: pending ? "blue-50" : "grey-50",
|
|
114
|
+
padding: "12px 12px 12px 12px",
|
|
115
|
+
vertical_spacing: "6px",
|
|
116
|
+
elements: [
|
|
117
|
+
{
|
|
118
|
+
tag: "markdown",
|
|
119
|
+
text_size: "notation",
|
|
120
|
+
content: `<font color='blue'>${safe(question.header)}</font>`,
|
|
121
|
+
},
|
|
122
|
+
{ tag: "markdown", content: `**${safe(question.question)}**` },
|
|
123
|
+
],
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
];
|
|
128
|
+
if (pending && answerAllowed && question.options.length > 0 && question.options.length <= 3) {
|
|
129
|
+
elements.push({
|
|
130
|
+
tag: "column_set",
|
|
131
|
+
element_id: "question_options",
|
|
132
|
+
flex_mode: "none",
|
|
133
|
+
horizontal_spacing: "8px",
|
|
134
|
+
columns: question.options.map((option, index) => answerButton(`answer_${index}`, option.label, index === 0 ? "primary_filled" : "default", requestId, question.id, option.label)),
|
|
135
|
+
});
|
|
136
|
+
const descriptions = question.options
|
|
137
|
+
.filter((option) => option.description)
|
|
138
|
+
.map((option) => `- **${safe(option.label)}**:${safe(option.description)}`);
|
|
139
|
+
if (descriptions.length > 0) {
|
|
140
|
+
elements.push({
|
|
141
|
+
tag: "markdown",
|
|
142
|
+
element_id: "option_descriptions",
|
|
143
|
+
text_size: "notation",
|
|
144
|
+
content: descriptions.join("\n"),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
else if (pending && answerAllowed && question.options.length > 0) {
|
|
149
|
+
elements.push({
|
|
150
|
+
tag: "select_static",
|
|
151
|
+
element_id: "question_select",
|
|
152
|
+
name: "runtime_answer",
|
|
153
|
+
width: "fill",
|
|
154
|
+
placeholder: { tag: "plain_text", content: "选择答案" },
|
|
155
|
+
options: question.options.map((option) => ({
|
|
156
|
+
text: { tag: "plain_text", content: truncatePlain(option.label, 70) },
|
|
157
|
+
value: option.label,
|
|
158
|
+
})),
|
|
159
|
+
behaviors: [
|
|
160
|
+
{
|
|
161
|
+
type: "callback",
|
|
162
|
+
value: {
|
|
163
|
+
bridge: "feishu-codex-v4",
|
|
164
|
+
action: "answer_runtime",
|
|
165
|
+
request_id: requestId,
|
|
166
|
+
question_id: question.id,
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
if (pending) {
|
|
173
|
+
elements.push({
|
|
174
|
+
tag: "markdown",
|
|
175
|
+
element_id: "answer_hint",
|
|
176
|
+
text_size: "notation",
|
|
177
|
+
content: question.isSecret
|
|
178
|
+
? "<font color='red'>安全保护:此桥接器不会通过飞书收集或转发密码、令牌、密钥等敏感答案。请回本机处理,或点击“取消回答”。</font>"
|
|
179
|
+
: question.isOther || question.options.length === 0
|
|
180
|
+
? "<font color='grey'>也可以直接发送下一条飞书消息作为答案。</font>"
|
|
181
|
+
: "<font color='grey'>点击选项,或直接发送下一条飞书消息作为答案。</font>",
|
|
182
|
+
});
|
|
183
|
+
elements.push({
|
|
184
|
+
tag: "column_set",
|
|
185
|
+
element_id: "question_actions",
|
|
186
|
+
flex_mode: "none",
|
|
187
|
+
columns: [
|
|
188
|
+
{
|
|
189
|
+
tag: "column",
|
|
190
|
+
width: "weighted",
|
|
191
|
+
weight: 1,
|
|
192
|
+
elements: [
|
|
193
|
+
{
|
|
194
|
+
tag: "button",
|
|
195
|
+
element_id: "cancel_answer",
|
|
196
|
+
text: { tag: "plain_text", content: "取消回答" },
|
|
197
|
+
type: "default",
|
|
198
|
+
width: "fill",
|
|
199
|
+
behaviors: [
|
|
200
|
+
{
|
|
201
|
+
type: "callback",
|
|
202
|
+
value: {
|
|
203
|
+
bridge: "feishu-codex-v4",
|
|
204
|
+
action: "cancel_runtime_question",
|
|
205
|
+
request_id: requestId,
|
|
206
|
+
question_id: question.id,
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
elements.push({
|
|
218
|
+
tag: "markdown",
|
|
219
|
+
element_id: "answer_result",
|
|
220
|
+
text_align: "center",
|
|
221
|
+
content: state === "answered"
|
|
222
|
+
? `<font color='green'>已回答:${safe(truncate(answer, 300))}</font>`
|
|
223
|
+
: state === "cancelled"
|
|
224
|
+
? "<font color='grey'>已取消回答,Codex 将继续处理</font>"
|
|
225
|
+
: "<font color='grey'>问题已过期,Codex 将继续处理。发送“任务”查看最新任务。</font>",
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
schema: "2.0",
|
|
230
|
+
config: {
|
|
231
|
+
update_multi: true,
|
|
232
|
+
width_mode: "default",
|
|
233
|
+
enable_forward: false,
|
|
234
|
+
summary: { content: `Codex 等待你的输入 · ${question.header}` },
|
|
235
|
+
},
|
|
236
|
+
header: {
|
|
237
|
+
title: { tag: "plain_text", content: "Codex 在等你回答" },
|
|
238
|
+
subtitle: {
|
|
239
|
+
tag: "plain_text",
|
|
240
|
+
content: `问题 ${position.index} / ${position.total} · ${ttlMinutes} 分钟内有效 · 回答后任务会自动继续`,
|
|
241
|
+
},
|
|
242
|
+
template: pending ? "blue" : state === "answered" ? "green" : "grey",
|
|
243
|
+
icon: { tag: "standard_icon", token: "myai_colorful" },
|
|
244
|
+
text_tag_list: [
|
|
245
|
+
{
|
|
246
|
+
tag: "text_tag",
|
|
247
|
+
text: { tag: "plain_text", content: "实时追问" },
|
|
248
|
+
color: pending ? "blue" : "neutral",
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
},
|
|
252
|
+
body: {
|
|
253
|
+
direction: "vertical",
|
|
254
|
+
padding: "12px 12px 20px 12px",
|
|
255
|
+
vertical_spacing: "12px",
|
|
256
|
+
elements,
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function actionButton(elementId, label, type, requestId, action, confirm) {
|
|
261
|
+
const button = {
|
|
262
|
+
tag: "button",
|
|
263
|
+
element_id: elementId,
|
|
264
|
+
text: { tag: "plain_text", content: label },
|
|
265
|
+
type,
|
|
266
|
+
width: "fill",
|
|
267
|
+
behaviors: [
|
|
268
|
+
{
|
|
269
|
+
type: "callback",
|
|
270
|
+
value: { bridge: "feishu-codex-v4", action, request_id: requestId },
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
};
|
|
274
|
+
if (confirm) {
|
|
275
|
+
button.confirm = {
|
|
276
|
+
title: { tag: "plain_text", content: confirm.title },
|
|
277
|
+
text: { tag: "plain_text", content: confirm.text },
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
return { tag: "column", width: "weighted", weight: 1, elements: [button] };
|
|
281
|
+
}
|
|
282
|
+
function answerButton(elementId, label, type, requestId, questionId, answer) {
|
|
283
|
+
return {
|
|
284
|
+
tag: "column",
|
|
285
|
+
width: "weighted",
|
|
286
|
+
weight: 1,
|
|
287
|
+
elements: [
|
|
288
|
+
{
|
|
289
|
+
tag: "button",
|
|
290
|
+
element_id: elementId,
|
|
291
|
+
text: { tag: "plain_text", content: truncatePlain(label, 40) },
|
|
292
|
+
type,
|
|
293
|
+
width: "fill",
|
|
294
|
+
behaviors: [
|
|
295
|
+
{
|
|
296
|
+
type: "callback",
|
|
297
|
+
value: {
|
|
298
|
+
bridge: "feishu-codex-v4",
|
|
299
|
+
action: "answer_runtime",
|
|
300
|
+
request_id: requestId,
|
|
301
|
+
question_id: questionId,
|
|
302
|
+
answer,
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
},
|
|
307
|
+
],
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
function safe(value) {
|
|
311
|
+
return redactSensitiveText(value)
|
|
312
|
+
.replaceAll("&", "&")
|
|
313
|
+
.replaceAll("<", "<")
|
|
314
|
+
.replaceAll(">", ">")
|
|
315
|
+
.replaceAll("*", "*")
|
|
316
|
+
.replaceAll("_", "_")
|
|
317
|
+
.replaceAll("`", "`")
|
|
318
|
+
.replaceAll("<at", "<at")
|
|
319
|
+
.replaceAll("</at>", "</at>");
|
|
320
|
+
}
|
|
321
|
+
function approvalKindLabel(kind) {
|
|
322
|
+
if (kind === "command")
|
|
323
|
+
return "命令执行";
|
|
324
|
+
if (kind === "file_change")
|
|
325
|
+
return "越界写入";
|
|
326
|
+
return "临时权限";
|
|
327
|
+
}
|
|
328
|
+
function truncate(value, max) {
|
|
329
|
+
if (value.length <= max)
|
|
330
|
+
return value;
|
|
331
|
+
return `${value.slice(0, max - 1)}…`;
|
|
332
|
+
}
|
|
333
|
+
function truncatePlain(value, max) {
|
|
334
|
+
if (value.length <= max)
|
|
335
|
+
return value;
|
|
336
|
+
return `${value.slice(0, max - 1)}…`;
|
|
337
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
export function renderSessionCenterCard(snapshot) {
|
|
2
|
+
const currentExists = snapshot.sessions.some((session) => session.id === snapshot.currentThreadId);
|
|
3
|
+
return {
|
|
4
|
+
schema: "2.0",
|
|
5
|
+
config: {
|
|
6
|
+
update_multi: true,
|
|
7
|
+
width_mode: "default",
|
|
8
|
+
enable_forward: false,
|
|
9
|
+
summary: { content: `Codex 会话中心 · ${safe(snapshot.projectName)}` },
|
|
10
|
+
},
|
|
11
|
+
header: {
|
|
12
|
+
title: { tag: "plain_text", content: "Codex 会话中心" },
|
|
13
|
+
subtitle: { tag: "plain_text", content: `恢复 ${snapshot.projectName} 的历史上下文` },
|
|
14
|
+
template: "blue",
|
|
15
|
+
icon: { tag: "standard_icon", token: "history_colorful" },
|
|
16
|
+
text_tag_list: [
|
|
17
|
+
{
|
|
18
|
+
tag: "text_tag",
|
|
19
|
+
text: { tag: "plain_text", content: `${snapshot.sessions.length} 个最近会话` },
|
|
20
|
+
color: "blue",
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
body: {
|
|
25
|
+
direction: "vertical",
|
|
26
|
+
padding: "12px 12px 18px 12px",
|
|
27
|
+
vertical_spacing: "12px",
|
|
28
|
+
elements: [
|
|
29
|
+
...(snapshot.feedback ? [notice(snapshot.feedback)] : []),
|
|
30
|
+
snapshot.sessions.length > 0
|
|
31
|
+
? {
|
|
32
|
+
tag: "select_static",
|
|
33
|
+
element_id: "session_select",
|
|
34
|
+
name: "thread_id",
|
|
35
|
+
width: "fill",
|
|
36
|
+
placeholder: { tag: "plain_text", content: "选择要恢复的会话" },
|
|
37
|
+
...(currentExists ? { initial_option: snapshot.currentThreadId } : {}),
|
|
38
|
+
options: snapshot.sessions.slice(0, 20).map((session) => ({
|
|
39
|
+
text: {
|
|
40
|
+
tag: "plain_text",
|
|
41
|
+
content: `${sessionTitle(session)} · ${relativeTime(session.updatedAt)}`.slice(0, 90),
|
|
42
|
+
},
|
|
43
|
+
value: session.id,
|
|
44
|
+
})),
|
|
45
|
+
behaviors: [callback("select_session")],
|
|
46
|
+
}
|
|
47
|
+
: {
|
|
48
|
+
tag: "markdown",
|
|
49
|
+
element_id: "session_empty",
|
|
50
|
+
content: "**还没有历史会话**\n<font color='grey'>发送第一条开发任务后,会话会自动保存在这里。</font>",
|
|
51
|
+
},
|
|
52
|
+
...snapshot.sessions.slice(0, 5).map((session, index) => sessionRow(session, index)),
|
|
53
|
+
actionGrid("session_actions", [
|
|
54
|
+
button("压缩当前会话", "session_compact", "default"),
|
|
55
|
+
button("新会话", "session_new", "primary"),
|
|
56
|
+
button("返回设置", "session_settings", "default"),
|
|
57
|
+
button("刷新", "session_refresh", "default"),
|
|
58
|
+
]),
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function sessionRow(session, index) {
|
|
64
|
+
return {
|
|
65
|
+
tag: "markdown",
|
|
66
|
+
element_id: `session_row_${index}`,
|
|
67
|
+
content: `**${safe(sessionTitle(session))}** ${statusTag(session.status)}\n<font color='grey'>${safe(session.preview || "暂无摘要").slice(0, 160)}</font>`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function notice(text) {
|
|
71
|
+
return {
|
|
72
|
+
tag: "markdown",
|
|
73
|
+
element_id: "session_feedback",
|
|
74
|
+
content: `<font color='blue'>${safe(text)}</font>`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function callback(action) {
|
|
78
|
+
return { type: "callback", value: { bridge: "feishu-codex-v5", action } };
|
|
79
|
+
}
|
|
80
|
+
function button(text, action, type) {
|
|
81
|
+
return {
|
|
82
|
+
tag: "button",
|
|
83
|
+
element_id: action.slice(0, 20),
|
|
84
|
+
text: { tag: "plain_text", content: text },
|
|
85
|
+
type: type === "primary" ? "primary_filled" : "default",
|
|
86
|
+
width: "fill",
|
|
87
|
+
behaviors: [callback(action)],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function actionGrid(id, actions) {
|
|
91
|
+
return {
|
|
92
|
+
tag: "column_set",
|
|
93
|
+
element_id: id,
|
|
94
|
+
flex_mode: "none",
|
|
95
|
+
horizontal_spacing: "8px",
|
|
96
|
+
columns: actions.map((action) => ({
|
|
97
|
+
tag: "column",
|
|
98
|
+
width: "weighted",
|
|
99
|
+
weight: 1,
|
|
100
|
+
elements: [action],
|
|
101
|
+
})),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function sessionTitle(session) {
|
|
105
|
+
return session.name || session.preview || session.id.slice(0, 8);
|
|
106
|
+
}
|
|
107
|
+
function relativeTime(timestamp) {
|
|
108
|
+
if (!timestamp)
|
|
109
|
+
return "未知时间";
|
|
110
|
+
const seconds = Math.max(0, Math.floor(Date.now() / 1_000 - timestamp));
|
|
111
|
+
if (seconds < 60)
|
|
112
|
+
return "刚刚";
|
|
113
|
+
if (seconds < 3_600)
|
|
114
|
+
return `${Math.floor(seconds / 60)} 分钟前`;
|
|
115
|
+
if (seconds < 86_400)
|
|
116
|
+
return `${Math.floor(seconds / 3_600)} 小时前`;
|
|
117
|
+
return `${Math.floor(seconds / 86_400)} 天前`;
|
|
118
|
+
}
|
|
119
|
+
function statusTag(status) {
|
|
120
|
+
if (status === "active")
|
|
121
|
+
return "<text_tag color='green'>活跃</text_tag>";
|
|
122
|
+
if (status === "error")
|
|
123
|
+
return "<text_tag color='red'>异常</text_tag>";
|
|
124
|
+
return "<text_tag color='neutral'>已保存</text_tag>";
|
|
125
|
+
}
|
|
126
|
+
function safe(value) {
|
|
127
|
+
return value.replaceAll("<", "<").replaceAll(">", ">").replaceAll("*", "*").replaceAll("`", "`");
|
|
128
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function sessionNameFromPrompt(prompt, projectName, maxLength = 56) {
|
|
2
|
+
const firstMeaningfulLine = prompt
|
|
3
|
+
.split(/\r?\n/)
|
|
4
|
+
.map((line) => line.trim())
|
|
5
|
+
.find(Boolean)
|
|
6
|
+
?.replace(/^[-*#>\s]+/, "")
|
|
7
|
+
.replace(/\s+/g, " ")
|
|
8
|
+
.trim();
|
|
9
|
+
const fallback = `${projectName} 新任务`;
|
|
10
|
+
const selected = firstMeaningfulLine || fallback;
|
|
11
|
+
if (selected.length <= maxLength)
|
|
12
|
+
return selected;
|
|
13
|
+
return `${selected.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`;
|
|
14
|
+
}
|
package/dist/smoke.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { CodexRunner } from "./codex-runner.js";
|
|
5
|
+
import { loadConfig } from "./config.js";
|
|
6
|
+
async function main() {
|
|
7
|
+
const config = loadConfig();
|
|
8
|
+
const runner = new CodexRunner({
|
|
9
|
+
...config,
|
|
10
|
+
sandboxMode: "read-only",
|
|
11
|
+
networkAccessEnabled: false,
|
|
12
|
+
webSearchMode: "disabled",
|
|
13
|
+
codexTimeoutMs: 120_000,
|
|
14
|
+
});
|
|
15
|
+
try {
|
|
16
|
+
const result = await runner.run("smoke-test", "smoke", "Reply with exactly BRIDGE_SMOKE_OK. Do not run commands and do not modify files.", config.workdir, existsSync(path.join(config.workdir, ".git")));
|
|
17
|
+
process.stdout.write(`${result.finalResponse.trim()}\n`);
|
|
18
|
+
if (result.finalResponse.trim() !== "BRIDGE_SMOKE_OK")
|
|
19
|
+
process.exitCode = 1;
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
await runner.close();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
main().catch((error) => {
|
|
26
|
+
console.error(error);
|
|
27
|
+
process.exitCode = 1;
|
|
28
|
+
});
|