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,272 @@
|
|
|
1
|
+
import { runbookHasDefaultInputs } from "./runbooks.js";
|
|
2
|
+
export function renderRunbookCenterCard(snapshot) {
|
|
3
|
+
const ready = snapshot.catalog.status === "ready";
|
|
4
|
+
return {
|
|
5
|
+
schema: "2.0",
|
|
6
|
+
config: {
|
|
7
|
+
update_multi: true,
|
|
8
|
+
width_mode: "default",
|
|
9
|
+
enable_forward: false,
|
|
10
|
+
summary: {
|
|
11
|
+
content: `Codex 团队运行手册 · ${snapshot.projectName} · ${snapshot.catalog.runbooks.length} 项`,
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
header: {
|
|
15
|
+
title: { tag: "plain_text", content: "Codex 团队运行手册" },
|
|
16
|
+
subtitle: {
|
|
17
|
+
tag: "plain_text",
|
|
18
|
+
content: "仓库内审核过的任务模板,一键运行也不会提升权限",
|
|
19
|
+
},
|
|
20
|
+
template: ready ? "violet" : "grey",
|
|
21
|
+
icon: { tag: "standard_icon", token: "tasklist_colorful" },
|
|
22
|
+
text_tag_list: [
|
|
23
|
+
{
|
|
24
|
+
tag: "text_tag",
|
|
25
|
+
text: { tag: "plain_text", content: safe(snapshot.projectName).slice(0, 28) },
|
|
26
|
+
color: "blue",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
tag: "text_tag",
|
|
30
|
+
text: {
|
|
31
|
+
tag: "plain_text",
|
|
32
|
+
content: ready ? `${snapshot.catalog.runbooks.length} 个模板` : statusLabel(snapshot.catalog.status),
|
|
33
|
+
},
|
|
34
|
+
color: ready ? "violet" : "neutral",
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
body: {
|
|
39
|
+
direction: "vertical",
|
|
40
|
+
padding: "12px 12px 18px 12px",
|
|
41
|
+
vertical_spacing: "12px",
|
|
42
|
+
elements: [
|
|
43
|
+
...(snapshot.feedback ? [feedbackPanel(snapshot.feedback)] : []),
|
|
44
|
+
sourcePanel(snapshot),
|
|
45
|
+
...(snapshot.catalog.runbooks.length > 0
|
|
46
|
+
? snapshot.catalog.runbooks.slice(0, 12).map((runbook, index) => runbookPanel(runbook, index, snapshot.canOperate))
|
|
47
|
+
: [emptyPanel(snapshot)]),
|
|
48
|
+
actionGrid(),
|
|
49
|
+
{
|
|
50
|
+
tag: "markdown",
|
|
51
|
+
element_id: "runbook_safety_note",
|
|
52
|
+
text_size: "notation",
|
|
53
|
+
text_align: "center",
|
|
54
|
+
content: "<font color='grey'>模板只能降低权限;提交、推送、部署和 PR 不能通过一键运行预授权。</font>",
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function sourcePanel(snapshot) {
|
|
61
|
+
const status = snapshot.catalog.status;
|
|
62
|
+
const detail = status === "ready"
|
|
63
|
+
? "配置已通过格式与安全校验;任务仍按成员角色、项目 ACL 和仓库策略执行。"
|
|
64
|
+
: status === "missing"
|
|
65
|
+
? "当前项目还没有运行手册。复制示例文件到仓库根目录并提交评审即可启用。"
|
|
66
|
+
: `配置未启用:${snapshot.catalog.error ?? "未知格式错误"}`;
|
|
67
|
+
return {
|
|
68
|
+
tag: "column_set",
|
|
69
|
+
element_id: "runbook_source",
|
|
70
|
+
flex_mode: "none",
|
|
71
|
+
columns: [
|
|
72
|
+
{
|
|
73
|
+
tag: "column",
|
|
74
|
+
width: "weighted",
|
|
75
|
+
weight: 1,
|
|
76
|
+
background_style: status === "invalid" ? "red-50" : status === "ready" ? "blue-50" : "grey-50",
|
|
77
|
+
padding: "10px 12px 10px 12px",
|
|
78
|
+
elements: [
|
|
79
|
+
{
|
|
80
|
+
tag: "markdown",
|
|
81
|
+
element_id: "runbook_source_text",
|
|
82
|
+
content: `**${statusLabel(status)}**\n<font color='grey'>${safe(detail)}</font>`,
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function runbookPanel(runbook, index, canOperate) {
|
|
90
|
+
const runnable = canOperate && runbookHasDefaultInputs(runbook);
|
|
91
|
+
const parameterHint = runbook.parameters.length > 0
|
|
92
|
+
? `参数 ${runbook.parameters.map((parameter) => parameter.label).join("、")}`
|
|
93
|
+
: "无需参数";
|
|
94
|
+
const settingHint = [
|
|
95
|
+
runbook.model ? `模型 ${runbook.model}` : "沿用模型",
|
|
96
|
+
runbook.reasoningEffort ? `推理 ${runbook.reasoningEffort}` : "沿用推理",
|
|
97
|
+
runbook.sandboxMode ? permissionLabel(runbook.sandboxMode) : "不提升权限",
|
|
98
|
+
].join(" · ");
|
|
99
|
+
const elements = [
|
|
100
|
+
{
|
|
101
|
+
tag: "markdown",
|
|
102
|
+
element_id: `runbook_text_${index}`,
|
|
103
|
+
content: `**${safe(runbook.name)}** <text_tag color='violet'>${safe(runbook.id)}</text_tag>\n` +
|
|
104
|
+
`${safe(runbook.description)}\n` +
|
|
105
|
+
`<font color='grey'>${safe(parameterHint)} · ${safe(settingHint)}</font>\n` +
|
|
106
|
+
`<font color='grey'>任务预览</font> ${safe(promptPreview(runbook.prompt))}`,
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
if (runnable) {
|
|
110
|
+
elements.push(runButton(runbook, index));
|
|
111
|
+
}
|
|
112
|
+
else if (!canOperate) {
|
|
113
|
+
elements.push({
|
|
114
|
+
tag: "markdown",
|
|
115
|
+
element_id: `runbook_readonly_${index}`,
|
|
116
|
+
text_size: "notation",
|
|
117
|
+
content: "<font color='grey'>只读成员不能启动运行手册。</font>",
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
elements.push({
|
|
122
|
+
tag: "markdown",
|
|
123
|
+
element_id: `runbook_usage_${index}`,
|
|
124
|
+
text_size: "notation",
|
|
125
|
+
content: `<font color='blue'>发送 /run ${safe(runbook.id)} ${safe(parameterUsage(runbook))}</font>`,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
tag: "column_set",
|
|
130
|
+
element_id: `runbook_${index}`,
|
|
131
|
+
flex_mode: "none",
|
|
132
|
+
columns: [
|
|
133
|
+
{
|
|
134
|
+
tag: "column",
|
|
135
|
+
width: "weighted",
|
|
136
|
+
weight: 1,
|
|
137
|
+
background_style: "grey-50",
|
|
138
|
+
padding: "12px 12px 12px 12px",
|
|
139
|
+
vertical_spacing: "8px",
|
|
140
|
+
elements,
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function runButton(runbook, index) {
|
|
146
|
+
return {
|
|
147
|
+
tag: "button",
|
|
148
|
+
element_id: `runbook_run_${index}`,
|
|
149
|
+
text: { tag: "plain_text", content: "使用默认参数运行" },
|
|
150
|
+
type: "primary_filled",
|
|
151
|
+
width: "fill",
|
|
152
|
+
confirm: {
|
|
153
|
+
title: { tag: "plain_text", content: `运行“${runbook.name}”?` },
|
|
154
|
+
text: {
|
|
155
|
+
tag: "plain_text",
|
|
156
|
+
content: "会在当前项目创建独立任务;实际权限不会高于你的当前设置。",
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
behaviors: [
|
|
160
|
+
{
|
|
161
|
+
type: "callback",
|
|
162
|
+
value: {
|
|
163
|
+
bridge: "feishu-codex-v7",
|
|
164
|
+
action: "runbook_run",
|
|
165
|
+
runbook_id: runbook.id,
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function emptyPanel(snapshot) {
|
|
172
|
+
return {
|
|
173
|
+
tag: "column_set",
|
|
174
|
+
element_id: "runbook_empty",
|
|
175
|
+
flex_mode: "none",
|
|
176
|
+
columns: [
|
|
177
|
+
{
|
|
178
|
+
tag: "column",
|
|
179
|
+
width: "weighted",
|
|
180
|
+
weight: 1,
|
|
181
|
+
background_style: "grey-50",
|
|
182
|
+
padding: "12px 12px 12px 12px",
|
|
183
|
+
elements: [
|
|
184
|
+
{
|
|
185
|
+
tag: "markdown",
|
|
186
|
+
content: snapshot.catalog.status === "invalid"
|
|
187
|
+
? "**运行手册已暂停**\n<font color='grey'>修复仓库配置后点击刷新;无效模板不会执行。</font>"
|
|
188
|
+
: "**从一个小模板开始**\n<font color='grey'>建议先加入测试、检查或生成文档类任务。</font>",
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function feedbackPanel(feedback) {
|
|
196
|
+
return {
|
|
197
|
+
tag: "column_set",
|
|
198
|
+
element_id: "runbook_feedback",
|
|
199
|
+
flex_mode: "none",
|
|
200
|
+
columns: [
|
|
201
|
+
{
|
|
202
|
+
tag: "column",
|
|
203
|
+
width: "weighted",
|
|
204
|
+
weight: 1,
|
|
205
|
+
background_style: "green-50",
|
|
206
|
+
padding: "10px 12px 10px 12px",
|
|
207
|
+
elements: [{ tag: "markdown", content: `<font color='green'>${safe(feedback)}</font>` }],
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function actionGrid() {
|
|
213
|
+
const actions = [
|
|
214
|
+
button("项目工作台", "runbook_projects"),
|
|
215
|
+
button("团队工作台", "runbook_team"),
|
|
216
|
+
button("刷新", "runbook_refresh"),
|
|
217
|
+
];
|
|
218
|
+
return {
|
|
219
|
+
tag: "column_set",
|
|
220
|
+
element_id: "runbook_actions",
|
|
221
|
+
flex_mode: "none",
|
|
222
|
+
horizontal_spacing: "8px",
|
|
223
|
+
columns: actions.map((action) => ({
|
|
224
|
+
tag: "column",
|
|
225
|
+
width: "weighted",
|
|
226
|
+
weight: 1,
|
|
227
|
+
elements: [action],
|
|
228
|
+
})),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function button(text, action) {
|
|
232
|
+
return {
|
|
233
|
+
tag: "button",
|
|
234
|
+
element_id: action,
|
|
235
|
+
text: { tag: "plain_text", content: text },
|
|
236
|
+
type: "default",
|
|
237
|
+
width: "fill",
|
|
238
|
+
behaviors: [
|
|
239
|
+
{
|
|
240
|
+
type: "callback",
|
|
241
|
+
value: { bridge: "feishu-codex-v7", action },
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function statusLabel(status) {
|
|
247
|
+
if (status === "ready")
|
|
248
|
+
return "配置已校验";
|
|
249
|
+
if (status === "missing")
|
|
250
|
+
return "尚未配置";
|
|
251
|
+
return "配置需修复";
|
|
252
|
+
}
|
|
253
|
+
function permissionLabel(mode) {
|
|
254
|
+
return mode === "read-only" ? "只读" : "工作区写入";
|
|
255
|
+
}
|
|
256
|
+
function promptPreview(prompt) {
|
|
257
|
+
const normalized = prompt.replace(/\s+/g, " ");
|
|
258
|
+
return normalized.length > 140 ? `${normalized.slice(0, 139)}…` : normalized;
|
|
259
|
+
}
|
|
260
|
+
function parameterUsage(runbook) {
|
|
261
|
+
return runbook.parameters
|
|
262
|
+
.map((parameter) => `${parameter.name}="${parameter.defaultValue ?? parameter.label}"`)
|
|
263
|
+
.join(" ");
|
|
264
|
+
}
|
|
265
|
+
function safe(value) {
|
|
266
|
+
return value
|
|
267
|
+
.replaceAll("<", "<")
|
|
268
|
+
.replaceAll(">", ">")
|
|
269
|
+
.replaceAll("*", "*")
|
|
270
|
+
.replaceAll("`", "`")
|
|
271
|
+
.slice(0, 1_000);
|
|
272
|
+
}
|
package/dist/runbooks.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { externalActionsForPrompt } from "./policy.js";
|
|
4
|
+
export const RUNBOOK_FILE = ".feishu-codex-runbooks.json";
|
|
5
|
+
const MAX_RUNBOOK_FILE_BYTES = 256 * 1024;
|
|
6
|
+
const MAX_RUNBOOKS = 30;
|
|
7
|
+
export async function loadProjectRunbooks(projectPath) {
|
|
8
|
+
const file = path.join(projectPath, RUNBOOK_FILE);
|
|
9
|
+
try {
|
|
10
|
+
const source = await readFile(file);
|
|
11
|
+
if (source.byteLength > MAX_RUNBOOK_FILE_BYTES) {
|
|
12
|
+
throw new Error(`${RUNBOOK_FILE} 不能超过 ${MAX_RUNBOOK_FILE_BYTES / 1024} KB`);
|
|
13
|
+
}
|
|
14
|
+
return { file, status: "ready", runbooks: parseRunbookCatalog(source.toString("utf8")) };
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
const code = error.code;
|
|
18
|
+
if (code === "ENOENT")
|
|
19
|
+
return { file, status: "missing", runbooks: [] };
|
|
20
|
+
return {
|
|
21
|
+
file,
|
|
22
|
+
status: "invalid",
|
|
23
|
+
runbooks: [],
|
|
24
|
+
error: error instanceof Error ? error.message : String(error),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function parseRunbookCatalog(source) {
|
|
29
|
+
let parsed;
|
|
30
|
+
try {
|
|
31
|
+
parsed = JSON.parse(source);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
throw new Error(`${RUNBOOK_FILE} 不是有效 JSON`);
|
|
35
|
+
}
|
|
36
|
+
if (!isRecord(parsed) || parsed.version !== 1 || !Array.isArray(parsed.runbooks)) {
|
|
37
|
+
throw new Error(`${RUNBOOK_FILE} 必须包含 version: 1 和 runbooks 数组`);
|
|
38
|
+
}
|
|
39
|
+
if (parsed.runbooks.length > MAX_RUNBOOKS) {
|
|
40
|
+
throw new Error(`每个项目最多配置 ${MAX_RUNBOOKS} 个运行手册`);
|
|
41
|
+
}
|
|
42
|
+
const ids = new Set();
|
|
43
|
+
return parsed.runbooks.map((raw, index) => {
|
|
44
|
+
if (!isRecord(raw))
|
|
45
|
+
throw new Error(`runbooks[${index}] 必须是对象`);
|
|
46
|
+
const id = requiredString(raw.id, `runbooks[${index}].id`, 40);
|
|
47
|
+
if (!/^[a-z0-9][a-z0-9_-]*$/i.test(id)) {
|
|
48
|
+
throw new Error(`运行手册 ID ${id} 只能包含字母、数字、下划线和连字符`);
|
|
49
|
+
}
|
|
50
|
+
if (ids.has(id))
|
|
51
|
+
throw new Error(`运行手册 ID 重复:${id}`);
|
|
52
|
+
ids.add(id);
|
|
53
|
+
const prompt = requiredString(raw.prompt, `${id}.prompt`, 8_000);
|
|
54
|
+
if (externalActionsForPrompt(prompt).length > 0) {
|
|
55
|
+
throw new Error(`${id}.prompt 包含提交、推送、部署或 PR 动作;一键运行手册不允许预授权外部动作`);
|
|
56
|
+
}
|
|
57
|
+
const sandboxMode = optionalEnum(raw.sandboxMode, ["read-only", "workspace-write"], `${id}.sandboxMode`);
|
|
58
|
+
const reasoningEffort = optionalEnum(raw.reasoningEffort, ["minimal", "low", "medium", "high", "xhigh", "ultra"], `${id}.reasoningEffort`);
|
|
59
|
+
const parameters = parseParameters(raw.parameters, id);
|
|
60
|
+
assertPromptParameters(prompt, parameters, id);
|
|
61
|
+
return {
|
|
62
|
+
id,
|
|
63
|
+
name: requiredString(raw.name, `${id}.name`, 60),
|
|
64
|
+
description: optionalString(raw.description, `${id}.description`, 180) ?? "团队审核过的任务模板",
|
|
65
|
+
prompt,
|
|
66
|
+
...(sandboxMode ? { sandboxMode } : {}),
|
|
67
|
+
...(typeof raw.model === "string" && raw.model.trim()
|
|
68
|
+
? { model: requiredString(raw.model, `${id}.model`, 100) }
|
|
69
|
+
: {}),
|
|
70
|
+
...(reasoningEffort ? { reasoningEffort } : {}),
|
|
71
|
+
parameters,
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
export function parseRunbookInvocation(prompt) {
|
|
76
|
+
const match = prompt
|
|
77
|
+
.trim()
|
|
78
|
+
.match(/^(?:运行手册|运行|run|\/run)\s+([a-z0-9][a-z0-9_-]*)(?:\s+(.+))?$/i);
|
|
79
|
+
if (!match?.[1])
|
|
80
|
+
return null;
|
|
81
|
+
const values = {};
|
|
82
|
+
const source = match[2]?.trim() ?? "";
|
|
83
|
+
if (source) {
|
|
84
|
+
const tokenPattern = /([a-z][a-z0-9_-]*)=("[^"]*"|'[^']*'|\S+)/gi;
|
|
85
|
+
let consumed = "";
|
|
86
|
+
for (const token of source.matchAll(tokenPattern)) {
|
|
87
|
+
const whole = token[0];
|
|
88
|
+
const name = token[1];
|
|
89
|
+
const rawValue = token[2];
|
|
90
|
+
if (!whole || !name || rawValue === undefined)
|
|
91
|
+
continue;
|
|
92
|
+
consumed += `${whole} `;
|
|
93
|
+
values[name] = unquote(rawValue).slice(0, 500);
|
|
94
|
+
}
|
|
95
|
+
const normalizedSource = source.replace(/\s+/g, " ").trim();
|
|
96
|
+
const normalizedConsumed = consumed.replace(/\s+/g, " ").trim();
|
|
97
|
+
if (normalizedSource !== normalizedConsumed)
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
return { id: match[1], values };
|
|
101
|
+
}
|
|
102
|
+
export function renderRunbookPrompt(runbook, values = {}) {
|
|
103
|
+
const known = new Set(runbook.parameters.map((parameter) => parameter.name));
|
|
104
|
+
const unknown = Object.keys(values).find((name) => !known.has(name));
|
|
105
|
+
if (unknown)
|
|
106
|
+
throw new Error(`运行手册不支持参数:${unknown}`);
|
|
107
|
+
let prompt = runbook.prompt;
|
|
108
|
+
for (const parameter of runbook.parameters) {
|
|
109
|
+
const value = values[parameter.name]?.trim() || parameter.defaultValue;
|
|
110
|
+
if (parameter.required && !value)
|
|
111
|
+
throw new Error(`缺少参数:${parameter.label}`);
|
|
112
|
+
const replacement = value ?? "";
|
|
113
|
+
prompt = prompt.replaceAll(`{{${parameter.name}}}`, replacement);
|
|
114
|
+
}
|
|
115
|
+
return prompt;
|
|
116
|
+
}
|
|
117
|
+
export function runbookHasDefaultInputs(runbook) {
|
|
118
|
+
return runbook.parameters.every((parameter) => !parameter.required || Boolean(parameter.defaultValue));
|
|
119
|
+
}
|
|
120
|
+
function parseParameters(value, runbookId) {
|
|
121
|
+
if (value === undefined)
|
|
122
|
+
return [];
|
|
123
|
+
if (!Array.isArray(value) || value.length > 12) {
|
|
124
|
+
throw new Error(`${runbookId}.parameters 必须是最多 12 项的数组`);
|
|
125
|
+
}
|
|
126
|
+
const names = new Set();
|
|
127
|
+
return value.map((raw, index) => {
|
|
128
|
+
if (!isRecord(raw))
|
|
129
|
+
throw new Error(`${runbookId}.parameters[${index}] 必须是对象`);
|
|
130
|
+
const name = requiredString(raw.name, `${runbookId}.parameters[${index}].name`, 40);
|
|
131
|
+
if (!/^[a-z][a-z0-9_-]*$/i.test(name)) {
|
|
132
|
+
throw new Error(`参数名 ${name} 格式无效`);
|
|
133
|
+
}
|
|
134
|
+
if (names.has(name))
|
|
135
|
+
throw new Error(`参数名重复:${name}`);
|
|
136
|
+
names.add(name);
|
|
137
|
+
const defaultValue = optionalString(raw.default, `${runbookId}.${name}.default`, 500);
|
|
138
|
+
return {
|
|
139
|
+
name,
|
|
140
|
+
label: requiredString(raw.label, `${runbookId}.${name}.label`, 60),
|
|
141
|
+
...(typeof raw.description === "string" && raw.description.trim()
|
|
142
|
+
? {
|
|
143
|
+
description: requiredString(raw.description, `${runbookId}.${name}.description`, 120),
|
|
144
|
+
}
|
|
145
|
+
: {}),
|
|
146
|
+
required: raw.required === true,
|
|
147
|
+
...(defaultValue !== undefined ? { defaultValue } : {}),
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
function assertPromptParameters(prompt, parameters, runbookId) {
|
|
152
|
+
const declared = new Set(parameters.map((parameter) => parameter.name));
|
|
153
|
+
const placeholders = [...prompt.matchAll(/\{\{([a-z][a-z0-9_-]*)\}\}/gi)].map((match) => match[1]);
|
|
154
|
+
const undeclared = placeholders.find((name) => !declared.has(name));
|
|
155
|
+
if (undeclared)
|
|
156
|
+
throw new Error(`${runbookId}.prompt 使用了未声明参数:${undeclared}`);
|
|
157
|
+
const unused = parameters.find((parameter) => !placeholders.includes(parameter.name));
|
|
158
|
+
if (unused)
|
|
159
|
+
throw new Error(`${runbookId}.parameters 声明了未使用参数:${unused.name}`);
|
|
160
|
+
}
|
|
161
|
+
function requiredString(value, name, max) {
|
|
162
|
+
if (typeof value !== "string" || !value.trim())
|
|
163
|
+
throw new Error(`${name} 必须是非空字符串`);
|
|
164
|
+
const normalized = value.trim();
|
|
165
|
+
if (normalized.length > max)
|
|
166
|
+
throw new Error(`${name} 不能超过 ${max} 个字符`);
|
|
167
|
+
return normalized;
|
|
168
|
+
}
|
|
169
|
+
function optionalString(value, name, max) {
|
|
170
|
+
if (value === undefined)
|
|
171
|
+
return undefined;
|
|
172
|
+
return requiredString(value, name, max);
|
|
173
|
+
}
|
|
174
|
+
function optionalEnum(value, allowed, name) {
|
|
175
|
+
if (value === undefined)
|
|
176
|
+
return undefined;
|
|
177
|
+
if (typeof value !== "string" || !allowed.includes(value)) {
|
|
178
|
+
throw new Error(`${name} 必须是:${allowed.join("、")}`);
|
|
179
|
+
}
|
|
180
|
+
return value;
|
|
181
|
+
}
|
|
182
|
+
function unquote(value) {
|
|
183
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
184
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
185
|
+
return value.slice(1, -1);
|
|
186
|
+
}
|
|
187
|
+
return value;
|
|
188
|
+
}
|
|
189
|
+
function isRecord(value) {
|
|
190
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
191
|
+
}
|