feat: gate claw runtime selection by availability
This commit is contained in:
@@ -57,7 +57,8 @@
|
||||
- `GET http://127.0.0.1:3000/api/v1/user/ota/package` 正常,当前会返回最新 APK 包
|
||||
- 当前这台开发机的 `launchd` 常驻 `local-agent` 已恢复:`GET http://127.0.0.1:4317/health` 现在可在数十毫秒内返回,且在手动 heartbeat 执行期间仍能正常回包
|
||||
- 当前 Boss 已新增 `src/lib/execution/` 执行底座抽象层;当前生产主链仍然沿用 `local-agent -> codex exec resume`,只是执行责任已开始通过 `ExecutionBackend / PromptAssembler / PermissionPolicy / RemoteRuntimeAdapter / OrchestrationBackend` 默认实现收束
|
||||
- 当前 `claw-code` 已以最小 `ClawBackendAdapter` 形式接入执行底座,但默认关闭;只有在显式配置 `BOSS_CLAW_*` 并在 `master-agent` 当前对话里显式选择 `claw-runtime` 时才会参与执行候选
|
||||
- 当前 `claw-code` 已以最小 `ClawBackendAdapter` 形式接入执行底座,但默认关闭;只有在显式配置 `BOSS_CLAW_*` 且可用性探测通过时,`master-agent` 当前对话里才会出现并允许选择 `claw-runtime`
|
||||
- 如果历史上已经保存过 `backendOverride=claw-runtime`,但当前 `Claw Runtime` 不可用,运行时会自动回退到默认后端,并在前台显示明确原因
|
||||
- 当前 `oh-my-codex` 仍未正式接入生产执行链;当前状态是 orchestration-ready,后续将通过独立 adapter 接入
|
||||
- 当前仓库已自带一个本地 smoke runtime:`scripts/claw-runtime-smoke.mjs`。在还没有真实 `claw-code` 可执行文件时,可以先用它验证 `ClawBackendAdapter -> backendOverride -> 异步回流` 整条链
|
||||
- `GET http://127.0.0.1:4317/api/v1/skills` 正常,已返回本机扫描到的 Codex Skill
|
||||
|
||||
@@ -15,9 +15,10 @@ import androidx.annotation.Nullable;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class MasterAgentPromptActivity extends BossScreenActivity {
|
||||
private static final String[] BACKEND_OVERRIDE_VALUES = {"", "claw-runtime"};
|
||||
private static final String[] BACKEND_OVERRIDE_LABELS = {"默认", "Claw Runtime"};
|
||||
public static final String EXTRA_PROJECT_ID = "project_id";
|
||||
public static final String EXTRA_PROJECT_NAME = "project_name";
|
||||
|
||||
@@ -31,6 +32,9 @@ public class MasterAgentPromptActivity extends BossScreenActivity {
|
||||
private @Nullable String userPromptText;
|
||||
private @Nullable String projectPromptOverrideText;
|
||||
private @Nullable String backendOverrideText;
|
||||
private boolean clawSelectable;
|
||||
private @Nullable String clawReasonLabel;
|
||||
private final List<String> backendOverrideValues = new ArrayList<>();
|
||||
private EditText userPromptInput;
|
||||
private EditText projectPromptInput;
|
||||
private Spinner backendSpinner;
|
||||
@@ -79,6 +83,7 @@ public class MasterAgentPromptActivity extends BossScreenActivity {
|
||||
promptPolicy = payload.optJSONObject("promptPolicy");
|
||||
userPrompt = payload.optJSONObject("userPrompt");
|
||||
projectControls = payload.optJSONObject("projectControls");
|
||||
JSONObject clawAvailability = payload.optJSONObject("clawAvailability");
|
||||
adminPromptText = promptPolicy == null ? null : promptPolicy.optString("globalPrompt", "");
|
||||
userPromptText = userPrompt == null ? "" : userPrompt.optString("content", "");
|
||||
projectPromptOverrideText = payload.optString(
|
||||
@@ -86,6 +91,8 @@ public class MasterAgentPromptActivity extends BossScreenActivity {
|
||||
projectControls == null ? "" : projectControls.optString("promptOverride", "")
|
||||
);
|
||||
backendOverrideText = projectControls == null ? "" : projectControls.optString("backendOverride", "");
|
||||
clawSelectable = clawAvailability != null && clawAvailability.optBoolean("selectable", false);
|
||||
clawReasonLabel = clawAvailability == null ? "" : clawAvailability.optString("reasonLabel", "");
|
||||
|
||||
replaceContent();
|
||||
appendContent(BossUi.buildSimpleProfileHeader(
|
||||
@@ -123,8 +130,17 @@ public class MasterAgentPromptActivity extends BossScreenActivity {
|
||||
projectPromptInput
|
||||
));
|
||||
|
||||
backendOverrideValues.clear();
|
||||
List<String> backendLabels = new ArrayList<>();
|
||||
backendOverrideValues.add("");
|
||||
backendLabels.add("默认");
|
||||
if (clawSelectable) {
|
||||
backendOverrideValues.add("claw-runtime");
|
||||
backendLabels.add("Claw Runtime");
|
||||
}
|
||||
|
||||
backendSpinner = new Spinner(this);
|
||||
backendSpinner.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, BACKEND_OVERRIDE_LABELS));
|
||||
backendSpinner.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, backendLabels));
|
||||
backendSpinner.setSelection(indexOfBackendOverride(backendOverrideText));
|
||||
backendSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
@@ -143,6 +159,16 @@ public class MasterAgentPromptActivity extends BossScreenActivity {
|
||||
"默认沿用 Boss 当前主链;需要时可显式切到 Claw Runtime。",
|
||||
backendSpinner
|
||||
));
|
||||
if (!clawSelectable) {
|
||||
appendContent(BossUi.buildSoftPanel(
|
||||
this,
|
||||
"Claw Runtime 当前不可用",
|
||||
TextUtils.isEmpty(clawReasonLabel) ? "当前环境未满足 Claw Runtime 的启动条件。" : clawReasonLabel,
|
||||
TextUtils.equals(backendOverrideText, "claw-runtime")
|
||||
? "当前对话之前保存过 Claw Runtime,运行时会自动回退到默认后端。"
|
||||
: "恢复可用后,执行后端下拉框会重新出现 Claw Runtime。"
|
||||
));
|
||||
}
|
||||
|
||||
previewTextView = new TextView(this);
|
||||
previewTextView.setText(buildPreviewText());
|
||||
@@ -203,9 +229,11 @@ public class MasterAgentPromptActivity extends BossScreenActivity {
|
||||
}
|
||||
String backendValue = backendSpinner == null
|
||||
? (backendOverrideText == null ? "" : backendOverrideText)
|
||||
: BACKEND_OVERRIDE_VALUES[backendSpinner.getSelectedItemPosition()];
|
||||
: backendOverrideValues.get(backendSpinner.getSelectedItemPosition());
|
||||
if (!TextUtils.isEmpty(backendValue)) {
|
||||
builder.append("【执行后端】\n").append(backendValue).append("\n\n");
|
||||
} else if (TextUtils.equals(backendOverrideText, "claw-runtime") && !clawSelectable) {
|
||||
builder.append("【执行后端】\n默认(Claw Runtime 当前不可用,运行时会自动回退)\n\n");
|
||||
}
|
||||
if (builder.length() == 0) {
|
||||
return "当前没有任何提示词内容。";
|
||||
@@ -222,7 +250,7 @@ public class MasterAgentPromptActivity extends BossScreenActivity {
|
||||
final String promptOverride = projectPromptInput == null ? "" : projectPromptInput.getText().toString();
|
||||
final String backendOverride = backendSpinner == null
|
||||
? ""
|
||||
: BACKEND_OVERRIDE_VALUES[backendSpinner.getSelectedItemPosition()];
|
||||
: backendOverrideValues.get(backendSpinner.getSelectedItemPosition());
|
||||
setRefreshing(true);
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
@@ -259,8 +287,8 @@ public class MasterAgentPromptActivity extends BossScreenActivity {
|
||||
if (TextUtils.isEmpty(value)) {
|
||||
return 0;
|
||||
}
|
||||
for (int index = 0; index < BACKEND_OVERRIDE_VALUES.length; index += 1) {
|
||||
if (value.equals(BACKEND_OVERRIDE_VALUES[index])) {
|
||||
for (int index = 0; index < backendOverrideValues.size(); index += 1) {
|
||||
if (value.equals(backendOverrideValues.get(index))) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,6 +156,43 @@ public class MasterAgentPromptActivityTest {
|
||||
assertTrue(viewTreeContainsText(content, "新的当前对话提示词"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void renderPromptProfileShowsClawUnavailableHintWhenBackendCannotBeSelected() throws Exception {
|
||||
TestMasterAgentPromptActivity activity = Robolectric
|
||||
.buildActivity(
|
||||
TestMasterAgentPromptActivity.class,
|
||||
new Intent()
|
||||
.putExtra(MasterAgentPromptActivity.EXTRA_PROJECT_ID, "master-agent")
|
||||
.putExtra(MasterAgentPromptActivity.EXTRA_PROJECT_NAME, "主 Agent")
|
||||
)
|
||||
.setup()
|
||||
.get();
|
||||
|
||||
JSONObject payload = new JSONObject()
|
||||
.put("promptPolicy", new JSONObject().put("globalPrompt", "全局主提示词"))
|
||||
.put("userPrompt", new JSONObject().put("content", "用户私有主提示词"))
|
||||
.put("projectControls", new JSONObject()
|
||||
.put("promptOverride", "当前对话提示词")
|
||||
.put("backendOverride", "claw-runtime"))
|
||||
.put("clawAvailability", new JSONObject()
|
||||
.put("status", "misconfigured")
|
||||
.put("selectable", false)
|
||||
.put("reason", "script_not_found")
|
||||
.put("reasonLabel", "未检测到有效的 Claw 启动脚本,将自动回退到默认后端。"));
|
||||
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"renderPromptProfile",
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, payload)
|
||||
);
|
||||
|
||||
Spinner backendSpinner = ReflectionHelpers.getField(activity, "backendSpinner");
|
||||
assertEquals(1, backendSpinner.getAdapter().getCount());
|
||||
View content = activity.findViewById(R.id.screen_content);
|
||||
assertTrue(viewTreeContainsText(content, "Claw Runtime 当前不可用"));
|
||||
assertTrue(viewTreeContainsText(content, "未检测到有效的 Claw 启动脚本"));
|
||||
}
|
||||
|
||||
private static boolean viewTreeContainsText(View root, String expectedText) {
|
||||
if (root instanceof TextView) {
|
||||
CharSequence text = ((TextView) root).getText();
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<array>
|
||||
<string>/bin/zsh</string>
|
||||
<string>-lc</string>
|
||||
<string>cd /Users/kris/code/boss && ./scripts/start-local-agent.sh __BOSS_AGENT_CONFIG__</string>
|
||||
<string>cd /Users/kris/code/boss && ./scripts/start-local-agent.sh __BOSS_AGENT_CONFIG__</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
|
||||
@@ -177,7 +177,8 @@
|
||||
- 当前状态:
|
||||
- 已在生产代码中被 `boss-master-agent.ts`、`local-agent/server.mjs` 和 `master-agent task complete route` 使用
|
||||
- 当前仍服务 Boss 自身执行链
|
||||
- 当前已最小接入 `ClawBackendAdapter`,但默认关闭,仅在显式配置和显式选择时参与执行
|
||||
- 当前已最小接入 `ClawBackendAdapter`,但默认关闭,仅在显式配置且可用性探测通过时才参与执行
|
||||
- 如果历史 `backendOverride=claw-runtime` 当前不可用,运行时会自动回退到默认后端,并把原因回给前台
|
||||
- 当前仓库自带 `scripts/claw-runtime-smoke.mjs` 作为兼容 JSON 协议的 smoke runtime,可用于本地和服务器验证 `ClawBackendAdapter`
|
||||
- 当前尚未接入 `oh-my-codex`
|
||||
|
||||
@@ -387,6 +388,7 @@
|
||||
- 当前只支持 `projectId=master-agent`
|
||||
- 仅 `highest_admin` 可写
|
||||
- `backendOverride` 当前仅支持 `claw-runtime`
|
||||
- 只有在 `Claw Runtime` 可用性探测通过时才允许保存 `claw-runtime`
|
||||
- 显式传 `null` 或空字符串表示清空覆盖;省略字段表示保留原值
|
||||
|
||||
#### `GET /api/v1/projects/[projectId]/participants`
|
||||
|
||||
@@ -28,7 +28,8 @@
|
||||
- `launchd` 已安装:`~/Library/LaunchAgents/com.hyzq.boss.local-agent.plist`
|
||||
- 当前执行底座抽象层已落地在 `src/lib/execution/`,并已补齐 `ExecutionBackend / PromptAssembler / PermissionPolicy / RemoteRuntimeAdapter / OrchestrationBackend` 默认实现
|
||||
- 当前生产主链仍然沿用 `local-agent -> codex exec resume -> /api/v1/master-agent/tasks/[taskId]/complete`,执行底座重构以“先抽象、不改行为”为准
|
||||
- 当前 `claw-code` 已以最小 `ClawBackendAdapter` 形式接入执行底座,但默认关闭;只有显式配置 `BOSS_CLAW_*` 并在 `master-agent` 当前对话中选择 `claw-runtime` 时才会参与执行候选
|
||||
- 当前 `claw-code` 已以最小 `ClawBackendAdapter` 形式接入执行底座,但默认关闭;只有显式配置 `BOSS_CLAW_*` 且可用性探测通过时,`master-agent` 当前对话中才会出现并允许选择 `claw-runtime`
|
||||
- 如果历史上已经保存过 `backendOverride=claw-runtime`,但当前 `Claw Runtime` 不可用,运行时会自动回退到默认后端,并在 Web/Android 前台给出明确原因
|
||||
- 当前仓库已自带 `scripts/claw-runtime-smoke.mjs` 作为本地 smoke runtime;在没有真实 `claw-code` 可执行文件时,可先用 `BOSS_CLAW_COMMAND=node` 与 `BOSS_CLAW_ARGS=scripts/claw-runtime-smoke.mjs` 验证整条链
|
||||
- 当前 `oh-my-codex` 还未正式接入生产链,只是已经具备 orchestration adapter-ready 的 contract 基础
|
||||
|
||||
@@ -146,7 +147,7 @@ cd /Users/kris/code/boss
|
||||
- 主 Agent 当前真实对话链路已验证通过:`Boss Web -> /api/v1/projects/master-agent/messages -> master-agent task queue -> local-agent -> codex exec -> /complete -> 项目消息账本`
|
||||
- 主 Agent 单聊当前已改成“快速入队 + 异步回流”:`POST /api/v1/projects/master-agent/messages` 会先返回 `masterReplyState + task`,真实回复随后再回写消息账本
|
||||
- 当前对话级 `agentControls` 已经生效:`master-agent` 会话支持 `modelOverride / reasoningEffortOverride`,并会优先作用到实际 OpenAI 回复和 Master Codex Node 执行 prompt
|
||||
- 当前对话级 `agentControls` 也已支持 `backendOverride`:`master-agent` 会话可显式选择 `claw-runtime`,由 `ExecutionBackendSelector` 在当前对话里优先尝试对应后端
|
||||
- 当前对话级 `agentControls` 也已支持 `backendOverride`:`master-agent` 会话可在 `Claw Runtime` 可用时显式选择 `claw-runtime`,由 `ExecutionBackendSelector` 在当前对话里优先尝试对应后端;不可用时保存接口会直接拒绝,并返回人类可读原因
|
||||
- 原生 Android 当前会把 `master-agent` 的等待态保留在消息流里:发送后常驻显示“主 Agent 思考中”,超时后改成“主 Agent 回复超时 + 重试等待”,收到新回复后会自动清掉,不再只靠 toast 提示
|
||||
- `GET /api/v1/app-logs` 当前已支持登录态分页查询
|
||||
- `POST /api/v1/app-logs`、`POST /api/v1/devices/[deviceId]/skills`、`POST /api/v1/workers/[workerId]/thread-context` 当前都要求有效设备 token 或匹配登录会话
|
||||
|
||||
@@ -25,6 +25,7 @@ config_path = sys.argv[2]
|
||||
text = plist_path.read_text()
|
||||
plist_path.write_text(text.replace("__BOSS_AGENT_CONFIG__", config_path))
|
||||
PY
|
||||
plutil -lint "$PLIST_TARGET" >/dev/null
|
||||
launchctl unload "$PLIST_TARGET" >/dev/null 2>&1 || true
|
||||
launchctl load "$PLIST_TARGET"
|
||||
echo "Loaded $PLIST_TARGET with $CONFIG_PATH"
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
hasPersistedProject,
|
||||
updateProjectAgentControls,
|
||||
} from "@/lib/boss-data";
|
||||
import { getClawBackendAvailability } from "@/lib/execution/backends/claw-config";
|
||||
|
||||
const reasoningEffortValues = new Set(["low", "medium", "high"]);
|
||||
|
||||
@@ -27,8 +28,11 @@ export async function GET(
|
||||
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
|
||||
}
|
||||
|
||||
const controls = await getProjectAgentControls(projectId, session.account);
|
||||
return NextResponse.json({ ok: true, controls });
|
||||
const [controls, clawAvailability] = await Promise.all([
|
||||
getProjectAgentControls(projectId, session.account),
|
||||
getClawBackendAvailability(),
|
||||
]);
|
||||
return NextResponse.json({ ok: true, controls, clawAvailability });
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
@@ -102,6 +106,16 @@ export async function POST(
|
||||
}
|
||||
|
||||
try {
|
||||
if (hasBackendOverride && payload.backendOverride === "claw-runtime") {
|
||||
const clawAvailability = await getClawBackendAvailability();
|
||||
if (!clawAvailability.selectable) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, message: clawAvailability.reasonLabel, clawAvailability },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const controls = await updateProjectAgentControls(
|
||||
projectId,
|
||||
{
|
||||
@@ -112,7 +126,11 @@ export async function POST(
|
||||
},
|
||||
session.account,
|
||||
);
|
||||
return NextResponse.json({ ok: true, controls: controls ?? null });
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
controls: controls ?? null,
|
||||
clawAvailability: await getClawBackendAvailability(),
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, message: error instanceof Error ? error.message : "UNKNOWN_ERROR" },
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
updateProjectAgentControls,
|
||||
updateUserMasterPrompt,
|
||||
} from "@/lib/boss-data";
|
||||
import { getClawBackendAvailability } from "@/lib/execution/backends/claw-config";
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
@@ -25,10 +26,11 @@ export async function GET(
|
||||
return NextResponse.json({ ok: false, message: "PROJECT_NOT_FOUND" }, { status: 404 });
|
||||
}
|
||||
|
||||
const [promptPolicy, userPrompt, projectControls] = await Promise.all([
|
||||
const [promptPolicy, userPrompt, projectControls, clawAvailability] = await Promise.all([
|
||||
getMasterAgentPromptPolicy(),
|
||||
getUserMasterPrompt(session.account),
|
||||
getProjectAgentControls(projectId, session.account),
|
||||
getClawBackendAvailability(),
|
||||
]);
|
||||
|
||||
return NextResponse.json({
|
||||
@@ -38,6 +40,7 @@ export async function GET(
|
||||
userPrompt,
|
||||
projectControls,
|
||||
projectPromptOverride: projectControls?.promptOverride ?? null,
|
||||
clawAvailability,
|
||||
account: session.account,
|
||||
});
|
||||
}
|
||||
@@ -105,6 +108,24 @@ export async function POST(
|
||||
}
|
||||
|
||||
try {
|
||||
if (
|
||||
hasBackendOverride &&
|
||||
typeof payload.backendOverride === "string" &&
|
||||
payload.backendOverride.trim() === "claw-runtime"
|
||||
) {
|
||||
const clawAvailability = await getClawBackendAvailability();
|
||||
if (!clawAvailability.selectable) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
ok: false,
|
||||
message: clawAvailability.reasonLabel,
|
||||
clawAvailability,
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasUserPromptContent) {
|
||||
const userPromptContent = typeof payload.userPromptContent === "string" ? payload.userPromptContent.trim() : "";
|
||||
if (userPromptContent) {
|
||||
@@ -121,10 +142,11 @@ export async function POST(
|
||||
}, session.account);
|
||||
}
|
||||
|
||||
const [promptPolicy, userPrompt, projectControls] = await Promise.all([
|
||||
const [promptPolicy, userPrompt, projectControls, clawAvailability] = await Promise.all([
|
||||
getMasterAgentPromptPolicy(),
|
||||
getUserMasterPrompt(session.account),
|
||||
getProjectAgentControls(projectId, session.account),
|
||||
getClawBackendAvailability(),
|
||||
]);
|
||||
|
||||
return NextResponse.json({
|
||||
@@ -134,6 +156,7 @@ export async function POST(
|
||||
userPrompt,
|
||||
projectControls,
|
||||
projectPromptOverride: projectControls?.promptOverride ?? null,
|
||||
clawAvailability,
|
||||
account: session.account,
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { AppShell, PageNav, StatusBar } from "@/components/app-ui";
|
||||
import { MasterAgentPromptMemoryClient } from "@/components/master-agent-prompt-memory-client";
|
||||
import { requirePageSession } from "@/lib/boss-auth";
|
||||
import { MASTER_AGENT_CHAT_PAGE_ANCHORS } from "@/lib/master-agent-chat-menu";
|
||||
import { getClawBackendAvailability } from "@/lib/execution/backends/claw-config";
|
||||
import {
|
||||
getMasterAgentPromptPolicy,
|
||||
getProjectAgentControls,
|
||||
@@ -13,13 +14,14 @@ export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function MasterAgentPromptMemoryPage() {
|
||||
const session = await requirePageSession();
|
||||
const [promptPolicy, userPrompt, projectControls, globalMemories, projectMemories] =
|
||||
const [promptPolicy, userPrompt, projectControls, globalMemories, projectMemories, clawAvailability] =
|
||||
await Promise.all([
|
||||
getMasterAgentPromptPolicy(),
|
||||
getUserMasterPrompt(session.account),
|
||||
getProjectAgentControls("master-agent", session.account),
|
||||
listUserMasterMemories(session.account, { includeArchived: false, scope: "global" }),
|
||||
listUserMasterMemories(session.account, { includeArchived: false, scope: "project" }),
|
||||
getClawBackendAvailability(),
|
||||
]);
|
||||
|
||||
return (
|
||||
@@ -42,6 +44,7 @@ export default async function MasterAgentPromptMemoryPage() {
|
||||
promptPolicy={promptPolicy}
|
||||
userPrompt={userPrompt}
|
||||
projectControls={projectControls}
|
||||
clawAvailability={clawAvailability}
|
||||
globalMemories={globalMemories}
|
||||
projectMemories={projectMemories}
|
||||
anchors={MASTER_AGENT_CHAT_PAGE_ANCHORS}
|
||||
|
||||
@@ -24,6 +24,13 @@ type MemoryDraft = {
|
||||
sourceMessageId: string;
|
||||
};
|
||||
|
||||
type ClawAvailability = {
|
||||
status: "disabled" | "misconfigured" | "ready";
|
||||
selectable: boolean;
|
||||
reason: string;
|
||||
reasonLabel: string;
|
||||
};
|
||||
|
||||
const memoryScopeOptions: Array<{ value: MasterMemoryScope; label: string }> = [
|
||||
{ value: "global", label: "通用记忆" },
|
||||
{ value: "project", label: "项目记忆" },
|
||||
@@ -145,6 +152,7 @@ export function MasterAgentPromptMemoryClient({
|
||||
promptPolicy,
|
||||
userPrompt,
|
||||
projectControls,
|
||||
clawAvailability,
|
||||
globalMemories,
|
||||
projectMemories,
|
||||
anchors,
|
||||
@@ -153,6 +161,7 @@ export function MasterAgentPromptMemoryClient({
|
||||
promptPolicy: MasterAgentPromptPolicy | null;
|
||||
userPrompt: UserMasterPrompt | null;
|
||||
projectControls: ProjectAgentControls | null;
|
||||
clawAvailability: ClawAvailability;
|
||||
globalMemories: MasterAgentMemory[];
|
||||
projectMemories: MasterAgentMemory[];
|
||||
anchors: MasterAgentChatPageAnchors;
|
||||
@@ -167,8 +176,10 @@ export function MasterAgentPromptMemoryClient({
|
||||
projectControls?.reasoningEffortOverride ?? "",
|
||||
);
|
||||
const [promptOverride, setPromptOverride] = useState(projectControls?.promptOverride ?? "");
|
||||
const storedClawOverrideUnavailable =
|
||||
projectControls?.backendOverride === "claw-runtime" && !clawAvailability.selectable;
|
||||
const [backendOverride, setBackendOverride] = useState(
|
||||
projectControls?.backendOverride === "claw-runtime" ? "claw-runtime" : "",
|
||||
projectControls?.backendOverride === "claw-runtime" && clawAvailability.selectable ? "claw-runtime" : "",
|
||||
);
|
||||
const [newMemory, setNewMemory] = useState<MemoryDraft>(makeNewMemoryDraft());
|
||||
const [memoryDrafts, setMemoryDrafts] = useState<Record<string, MemoryDraft>>(() => {
|
||||
@@ -185,9 +196,14 @@ export function MasterAgentPromptMemoryClient({
|
||||
globalPrompt.trim() ? `【管理员全局主提示词】\n${globalPrompt.trim()}` : null,
|
||||
userPromptContent.trim() ? `【用户私有主提示词】\n${userPromptContent.trim()}` : null,
|
||||
promptOverride.trim() ? `【当前对话附加提示词】\n${promptOverride.trim()}` : null,
|
||||
backendOverride.trim()
|
||||
? `【执行后端】\n${backendOverride.trim()}`
|
||||
: storedClawOverrideUnavailable
|
||||
? "【执行后端】\n默认(Claw Runtime 当前不可用,运行时会自动回退)"
|
||||
: null,
|
||||
].filter(Boolean);
|
||||
return sections.length > 0 ? sections.join("\n\n") : "当前还没有组合后的提示词内容。";
|
||||
}, [globalPrompt, userPromptContent, promptOverride]);
|
||||
}, [backendOverride, globalPrompt, promptOverride, storedClawOverrideUnavailable, userPromptContent]);
|
||||
|
||||
function updateMemoryDraft(memoryId: string, updater: (draft: MemoryDraft) => MemoryDraft) {
|
||||
setMemoryDrafts((current) => ({
|
||||
@@ -441,10 +457,21 @@ export function MasterAgentPromptMemoryClient({
|
||||
className="w-full rounded-xl border border-[#E5E5EA] bg-[#F7F8FA] px-3 py-2 text-[13px] text-[#111111] outline-none"
|
||||
>
|
||||
<option value="">默认</option>
|
||||
<option value="claw-runtime">Claw Runtime</option>
|
||||
{clawAvailability.selectable ? <option value="claw-runtime">Claw Runtime</option> : null}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
{!clawAvailability.selectable ? (
|
||||
<div className="rounded-2xl border border-[#F4C7C3] bg-[#FFF7F6] px-4 py-3 text-[12px] leading-6 text-[#B54708]">
|
||||
<div className="font-semibold text-[#912018]">Claw Runtime 当前不可用</div>
|
||||
<div>{clawAvailability.reasonLabel}</div>
|
||||
{storedClawOverrideUnavailable ? (
|
||||
<div className="mt-1 text-[#912018]">
|
||||
当前对话之前保存过 Claw Runtime,运行时会自动回退到默认后端。
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<TextArea
|
||||
label="当前对话附加提示词"
|
||||
value={promptOverride}
|
||||
|
||||
@@ -1738,6 +1738,7 @@ export async function replyToMasterAgentUserMessage(params: {
|
||||
runtime.account.provider === "master_codex_node" && (!primaryDevice || primaryDevice.status !== "online")
|
||||
? "degraded"
|
||||
: runtime.account.status;
|
||||
const clawSelectionState = await getClawBackendSelectionState();
|
||||
const backendSelectionInput = {
|
||||
primary: {
|
||||
provider: runtime.account.provider,
|
||||
@@ -1751,7 +1752,7 @@ export async function replyToMasterAgentUserMessage(params: {
|
||||
})),
|
||||
requestKind: "master_agent_reply" as const,
|
||||
requestedBackendId: executionConfig.agentControls?.backendOverride,
|
||||
claw: getClawBackendSelectionState(),
|
||||
claw: clawSelectionState,
|
||||
};
|
||||
const selectedBackend = await selectExecutionBackend(backendSelectionInput);
|
||||
const backendChoices = listExecutionBackendChoices(backendSelectionInput);
|
||||
|
||||
@@ -51,7 +51,7 @@ function resolveBackendByProvider(provider: AiProvider): ExecutionBackendChoice
|
||||
function isReadyBackend(choice: ExecutionBackendChoice, input: ExecutionBackendSelectionInput) {
|
||||
if (choice.backendId === CLAW_BACKEND.backendId) {
|
||||
const requestKind = input.requestKind;
|
||||
if (!input.claw?.enabled || !requestKind) {
|
||||
if (!input.claw?.selectable || !requestKind) {
|
||||
return false;
|
||||
}
|
||||
return isClawRequestKindSupported(requestKind);
|
||||
|
||||
@@ -6,8 +6,10 @@ import type {
|
||||
ExecutionRequestKind,
|
||||
} from "@/lib/execution/types";
|
||||
import {
|
||||
getClawBackendAvailability,
|
||||
getClawBackendConfig,
|
||||
isClawBackendConfigured,
|
||||
type ClawBackendAvailability,
|
||||
type ClawBackendConfig,
|
||||
} from "@/lib/execution/backends/claw-config";
|
||||
import { runClawCommand } from "@/lib/execution/backends/claw-runner";
|
||||
@@ -30,6 +32,8 @@ type ClawRunner = (input: ClawRunnerInput) => Promise<ExecutionImmediateResult>;
|
||||
|
||||
export interface ClawBackendSelectionState {
|
||||
enabled: boolean;
|
||||
selectable: boolean;
|
||||
availability: ClawBackendAvailability;
|
||||
supportsKinds: ExecutionRequestKind[];
|
||||
}
|
||||
|
||||
@@ -45,11 +49,14 @@ export function isClawRequestKindSupported(kind: ExecutionRequestKind) {
|
||||
return SUPPORTED_CLAW_KINDS.has(kind);
|
||||
}
|
||||
|
||||
export function getClawBackendSelectionState(
|
||||
export async function getClawBackendSelectionState(
|
||||
config: ClawBackendConfig = getClawBackendConfig(),
|
||||
): ClawBackendSelectionState {
|
||||
): Promise<ClawBackendSelectionState> {
|
||||
const availability = await getClawBackendAvailability(config);
|
||||
return {
|
||||
enabled: isClawBackendConfigured(config),
|
||||
selectable: availability.selectable,
|
||||
availability,
|
||||
supportsKinds: [...SUPPORTED_CLAW_KINDS],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import { access } from "node:fs/promises";
|
||||
import { constants } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
export interface ClawBackendConfig {
|
||||
enabled: boolean;
|
||||
command?: string;
|
||||
@@ -7,6 +11,24 @@ export interface ClawBackendConfig {
|
||||
defaultModel?: string;
|
||||
}
|
||||
|
||||
export type ClawBackendAvailabilityStatus = "disabled" | "misconfigured" | "ready";
|
||||
|
||||
export interface ClawBackendAvailability {
|
||||
status: ClawBackendAvailabilityStatus;
|
||||
selectable: boolean;
|
||||
configured: boolean;
|
||||
reason:
|
||||
| "disabled"
|
||||
| "command_not_set"
|
||||
| "command_not_found"
|
||||
| "workdir_not_found"
|
||||
| "script_not_found"
|
||||
| "ready";
|
||||
reasonLabel: string;
|
||||
command?: string;
|
||||
cwd?: string;
|
||||
}
|
||||
|
||||
function parseBoolean(value: string | undefined) {
|
||||
return value?.trim().toLowerCase() === "true";
|
||||
}
|
||||
@@ -38,5 +60,127 @@ export function isClawBackendConfigured(config: ClawBackendConfig) {
|
||||
return config.enabled && Boolean(config.command);
|
||||
}
|
||||
|
||||
function commandLooksLikePath(command: string) {
|
||||
return command.includes("/") || command.includes("\\");
|
||||
}
|
||||
|
||||
async function fileExists(filePath: string, mode = constants.F_OK) {
|
||||
try {
|
||||
await access(filePath, mode);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveScriptCandidate(config: ClawBackendConfig) {
|
||||
if (!config.command || config.args.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const commandName = path.basename(config.command).toLowerCase();
|
||||
const scriptRuntimes = new Set(["node", "node.exe", "tsx", "tsx.cmd", "bun", "bun.exe", "deno", "deno.exe"]);
|
||||
if (!scriptRuntimes.has(commandName)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const candidate = config.args[0];
|
||||
if (!candidate || candidate.startsWith("-")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return path.isAbsolute(candidate)
|
||||
? candidate
|
||||
: path.resolve(config.cwd?.trim() || process.cwd(), candidate);
|
||||
}
|
||||
|
||||
async function isCommandReachable(command: string) {
|
||||
if (commandLooksLikePath(command)) {
|
||||
return fileExists(path.resolve(command), constants.X_OK);
|
||||
}
|
||||
|
||||
const searchPaths = (process.env.PATH || "")
|
||||
.split(path.delimiter)
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean);
|
||||
for (const entry of searchPaths) {
|
||||
const candidate = path.join(entry, command);
|
||||
if (await fileExists(candidate, constants.X_OK)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function getClawBackendAvailability(
|
||||
config: ClawBackendConfig = getClawBackendConfig(),
|
||||
): Promise<ClawBackendAvailability> {
|
||||
const base = {
|
||||
command: config.command,
|
||||
cwd: config.cwd,
|
||||
configured: isClawBackendConfigured(config),
|
||||
};
|
||||
|
||||
if (!config.enabled) {
|
||||
return {
|
||||
...base,
|
||||
status: "disabled",
|
||||
selectable: false,
|
||||
reason: "disabled",
|
||||
reasonLabel: "Claw Runtime 当前未启用。",
|
||||
};
|
||||
}
|
||||
|
||||
if (!config.command) {
|
||||
return {
|
||||
...base,
|
||||
status: "misconfigured",
|
||||
selectable: false,
|
||||
reason: "command_not_set",
|
||||
reasonLabel: "Claw Runtime 缺少启动命令。",
|
||||
};
|
||||
}
|
||||
|
||||
if (!(await isCommandReachable(config.command))) {
|
||||
return {
|
||||
...base,
|
||||
status: "misconfigured",
|
||||
selectable: false,
|
||||
reason: "command_not_found",
|
||||
reasonLabel: "未检测到可执行的 Claw 启动命令。",
|
||||
};
|
||||
}
|
||||
|
||||
if (config.cwd && !(await fileExists(config.cwd, constants.F_OK))) {
|
||||
return {
|
||||
...base,
|
||||
status: "misconfigured",
|
||||
selectable: false,
|
||||
reason: "workdir_not_found",
|
||||
reasonLabel: "Claw Runtime 工作目录不存在。",
|
||||
};
|
||||
}
|
||||
|
||||
const scriptCandidate = resolveScriptCandidate(config);
|
||||
if (scriptCandidate && !(await fileExists(scriptCandidate, constants.F_OK))) {
|
||||
return {
|
||||
...base,
|
||||
status: "misconfigured",
|
||||
selectable: false,
|
||||
reason: "script_not_found",
|
||||
reasonLabel: "未检测到有效的 Claw 启动脚本,将自动回退到默认后端。",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...base,
|
||||
status: "ready",
|
||||
selectable: true,
|
||||
reason: "ready",
|
||||
reasonLabel: "Claw Runtime 可用。",
|
||||
};
|
||||
}
|
||||
|
||||
export const getClawBackendConfigForTesting = getClawBackendConfig;
|
||||
export const isClawBackendConfiguredForTesting = isClawBackendConfigured;
|
||||
export const getClawBackendAvailabilityForTesting = getClawBackendAvailability;
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import {
|
||||
getClawBackendConfigForTesting,
|
||||
getClawBackendAvailabilityForTesting,
|
||||
isClawBackendConfiguredForTesting,
|
||||
} from "../src/lib/execution/backends/claw-config.ts";
|
||||
|
||||
@@ -63,3 +67,48 @@ test("Claw backend 在配置完整时返回 command、args 和 timeout", () => {
|
||||
|
||||
restoreEnv(previous);
|
||||
});
|
||||
|
||||
test("Claw backend availability 会在可执行命令和脚本都存在时返回 ready", async () => {
|
||||
const previous = snapshotEnv();
|
||||
const tempDir = await mkdtemp(path.join(os.tmpdir(), "boss-claw-config-"));
|
||||
const scriptPath = path.join(tempDir, "claw-smoke.mjs");
|
||||
await writeFile(scriptPath, "console.log('ok');\n", "utf8");
|
||||
|
||||
process.env.BOSS_CLAW_ENABLED = "true";
|
||||
process.env.BOSS_CLAW_COMMAND = process.execPath;
|
||||
process.env.BOSS_CLAW_ARGS = scriptPath;
|
||||
process.env.BOSS_CLAW_WORKDIR = tempDir;
|
||||
|
||||
try {
|
||||
const availability = await getClawBackendAvailabilityForTesting();
|
||||
|
||||
assert.equal(availability.status, "ready");
|
||||
assert.equal(availability.selectable, true);
|
||||
assert.equal(availability.reason, "ready");
|
||||
} finally {
|
||||
restoreEnv(previous);
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("Claw backend availability 会在脚本参数不存在时返回不可选", async () => {
|
||||
const previous = snapshotEnv();
|
||||
const tempDir = await mkdtemp(path.join(os.tmpdir(), "boss-claw-config-"));
|
||||
const missingScript = path.join(tempDir, "missing-claw-script.mjs");
|
||||
|
||||
process.env.BOSS_CLAW_ENABLED = "true";
|
||||
process.env.BOSS_CLAW_COMMAND = process.execPath;
|
||||
process.env.BOSS_CLAW_ARGS = missingScript;
|
||||
process.env.BOSS_CLAW_WORKDIR = tempDir;
|
||||
|
||||
try {
|
||||
const availability = await getClawBackendAvailabilityForTesting();
|
||||
|
||||
assert.equal(availability.status, "misconfigured");
|
||||
assert.equal(availability.selectable, false);
|
||||
assert.equal(availability.reason, "script_not_found");
|
||||
} finally {
|
||||
restoreEnv(previous);
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -98,6 +98,14 @@ test("selectExecutionBackendForTesting honors an explicit claw request when claw
|
||||
requestedBackendId: "claw-runtime",
|
||||
claw: {
|
||||
enabled: true,
|
||||
selectable: true,
|
||||
availability: {
|
||||
status: "ready",
|
||||
selectable: true,
|
||||
configured: true,
|
||||
reason: "ready",
|
||||
reasonLabel: "Claw Runtime 可用。",
|
||||
},
|
||||
supportsKinds: ["master_agent_reply", "thread_reply"],
|
||||
},
|
||||
});
|
||||
@@ -113,6 +121,14 @@ test("selectExecutionBackendForTesting falls back when claw is requested but una
|
||||
requestedBackendId: "claw-runtime",
|
||||
claw: {
|
||||
enabled: false,
|
||||
selectable: false,
|
||||
availability: {
|
||||
status: "disabled",
|
||||
selectable: false,
|
||||
configured: false,
|
||||
reason: "disabled",
|
||||
reasonLabel: "Claw Runtime 当前未启用。",
|
||||
},
|
||||
supportsKinds: ["master_agent_reply"],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { execFile as execFileCallback } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { NextRequest } from "next/server";
|
||||
@@ -93,92 +93,117 @@ test("master-agent 会话可保存并读取模型与推理强度覆盖", async (
|
||||
|
||||
test("master-agent 对话控制路由可读写并回显到项目详情", async () => {
|
||||
await setup();
|
||||
|
||||
const session = await createAuthSession({
|
||||
account: "17600003315",
|
||||
role: "highest_admin",
|
||||
displayName: "Boss 超级管理员",
|
||||
loginMethod: "password",
|
||||
});
|
||||
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
cookie: `${AUTH_SESSION_COOKIE}=${session.sessionToken}`,
|
||||
const tempDir = await mkdtemp(path.join(os.tmpdir(), "boss-claw-agent-controls-"));
|
||||
const scriptPath = path.join(tempDir, "claw-runtime.mjs");
|
||||
await writeFile(scriptPath, "console.log('ok');\n", "utf8");
|
||||
const previousEnv = {
|
||||
BOSS_CLAW_ENABLED: process.env.BOSS_CLAW_ENABLED,
|
||||
BOSS_CLAW_COMMAND: process.env.BOSS_CLAW_COMMAND,
|
||||
BOSS_CLAW_ARGS: process.env.BOSS_CLAW_ARGS,
|
||||
BOSS_CLAW_WORKDIR: process.env.BOSS_CLAW_WORKDIR,
|
||||
};
|
||||
process.env.BOSS_CLAW_ENABLED = "true";
|
||||
process.env.BOSS_CLAW_COMMAND = process.execPath;
|
||||
process.env.BOSS_CLAW_ARGS = scriptPath;
|
||||
process.env.BOSS_CLAW_WORKDIR = tempDir;
|
||||
|
||||
const postResponse = await postAgentControlsRoute(
|
||||
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
modelOverride: "gpt-5.4",
|
||||
reasoningEffortOverride: "medium",
|
||||
backendOverride: "claw-runtime",
|
||||
try {
|
||||
const session = await createAuthSession({
|
||||
account: "17600003315",
|
||||
role: "highest_admin",
|
||||
displayName: "Boss 超级管理员",
|
||||
loginMethod: "password",
|
||||
});
|
||||
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
cookie: `${AUTH_SESSION_COOKIE}=${session.sessionToken}`,
|
||||
};
|
||||
|
||||
const postResponse = await postAgentControlsRoute(
|
||||
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
modelOverride: "gpt-5.4",
|
||||
reasoningEffortOverride: "medium",
|
||||
backendOverride: "claw-runtime",
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
||||
);
|
||||
assert.equal(postResponse.status, 200);
|
||||
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
||||
);
|
||||
assert.equal(postResponse.status, 200);
|
||||
|
||||
const postPayload = (await postResponse.json()) as {
|
||||
ok: boolean;
|
||||
controls: {
|
||||
modelOverride?: string;
|
||||
reasoningEffortOverride?: string;
|
||||
backendOverride?: string;
|
||||
updatedAt: string;
|
||||
} | null;
|
||||
};
|
||||
assert.equal(postPayload.ok, true);
|
||||
assert.equal(postPayload.controls?.modelOverride, "gpt-5.4");
|
||||
assert.equal(postPayload.controls?.reasoningEffortOverride, "medium");
|
||||
assert.equal(postPayload.controls?.backendOverride, "claw-runtime");
|
||||
const postPayload = (await postResponse.json()) as {
|
||||
ok: boolean;
|
||||
controls: {
|
||||
modelOverride?: string;
|
||||
reasoningEffortOverride?: string;
|
||||
backendOverride?: string;
|
||||
updatedAt: string;
|
||||
} | null;
|
||||
};
|
||||
assert.equal(postPayload.ok, true);
|
||||
assert.equal(postPayload.controls?.modelOverride, "gpt-5.4");
|
||||
assert.equal(postPayload.controls?.reasoningEffortOverride, "medium");
|
||||
assert.equal(postPayload.controls?.backendOverride, "claw-runtime");
|
||||
|
||||
const getResponse = await getAgentControlsRoute(
|
||||
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
|
||||
method: "GET",
|
||||
headers,
|
||||
}),
|
||||
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
||||
);
|
||||
assert.equal(getResponse.status, 200);
|
||||
const getResponse = await getAgentControlsRoute(
|
||||
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
|
||||
method: "GET",
|
||||
headers,
|
||||
}),
|
||||
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
||||
);
|
||||
assert.equal(getResponse.status, 200);
|
||||
|
||||
const getPayload = (await getResponse.json()) as {
|
||||
ok: boolean;
|
||||
controls: {
|
||||
modelOverride?: string;
|
||||
reasoningEffortOverride?: string;
|
||||
backendOverride?: string;
|
||||
updatedAt: string;
|
||||
} | null;
|
||||
};
|
||||
assert.equal(getPayload.ok, true);
|
||||
assert.equal(getPayload.controls?.modelOverride, "gpt-5.4");
|
||||
assert.equal(getPayload.controls?.reasoningEffortOverride, "medium");
|
||||
assert.equal(getPayload.controls?.backendOverride, "claw-runtime");
|
||||
const getPayload = (await getResponse.json()) as {
|
||||
ok: boolean;
|
||||
controls: {
|
||||
modelOverride?: string;
|
||||
reasoningEffortOverride?: string;
|
||||
backendOverride?: string;
|
||||
updatedAt: string;
|
||||
} | null;
|
||||
};
|
||||
assert.equal(getPayload.ok, true);
|
||||
assert.equal(getPayload.controls?.modelOverride, "gpt-5.4");
|
||||
assert.equal(getPayload.controls?.reasoningEffortOverride, "medium");
|
||||
assert.equal(getPayload.controls?.backendOverride, "claw-runtime");
|
||||
|
||||
const projectResponse = await getProjectRoute(
|
||||
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent", {
|
||||
method: "GET",
|
||||
headers,
|
||||
}),
|
||||
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
||||
);
|
||||
assert.equal(projectResponse.status, 200);
|
||||
const projectResponse = await getProjectRoute(
|
||||
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent", {
|
||||
method: "GET",
|
||||
headers,
|
||||
}),
|
||||
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
||||
);
|
||||
assert.equal(projectResponse.status, 200);
|
||||
|
||||
const projectPayload = (await projectResponse.json()) as {
|
||||
ok: boolean;
|
||||
agentControls: {
|
||||
modelOverride?: string;
|
||||
reasoningEffortOverride?: string;
|
||||
backendOverride?: string;
|
||||
updatedAt: string;
|
||||
} | null;
|
||||
};
|
||||
assert.equal(projectPayload.ok, true);
|
||||
assert.equal(projectPayload.agentControls?.modelOverride, "gpt-5.4");
|
||||
assert.equal(projectPayload.agentControls?.reasoningEffortOverride, "medium");
|
||||
assert.equal(projectPayload.agentControls?.backendOverride, "claw-runtime");
|
||||
const projectPayload = (await projectResponse.json()) as {
|
||||
ok: boolean;
|
||||
agentControls: {
|
||||
modelOverride?: string;
|
||||
reasoningEffortOverride?: string;
|
||||
backendOverride?: string;
|
||||
updatedAt: string;
|
||||
} | null;
|
||||
};
|
||||
assert.equal(projectPayload.ok, true);
|
||||
assert.equal(projectPayload.agentControls?.modelOverride, "gpt-5.4");
|
||||
assert.equal(projectPayload.agentControls?.reasoningEffortOverride, "medium");
|
||||
assert.equal(projectPayload.agentControls?.backendOverride, "claw-runtime");
|
||||
} finally {
|
||||
if (previousEnv.BOSS_CLAW_ENABLED === undefined) delete process.env.BOSS_CLAW_ENABLED;
|
||||
else process.env.BOSS_CLAW_ENABLED = previousEnv.BOSS_CLAW_ENABLED;
|
||||
if (previousEnv.BOSS_CLAW_COMMAND === undefined) delete process.env.BOSS_CLAW_COMMAND;
|
||||
else process.env.BOSS_CLAW_COMMAND = previousEnv.BOSS_CLAW_COMMAND;
|
||||
if (previousEnv.BOSS_CLAW_ARGS === undefined) delete process.env.BOSS_CLAW_ARGS;
|
||||
else process.env.BOSS_CLAW_ARGS = previousEnv.BOSS_CLAW_ARGS;
|
||||
if (previousEnv.BOSS_CLAW_WORKDIR === undefined) delete process.env.BOSS_CLAW_WORKDIR;
|
||||
else process.env.BOSS_CLAW_WORKDIR = previousEnv.BOSS_CLAW_WORKDIR;
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("master-agent 对话控制按当前账号隔离,不会串到其他用户", async () => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
let runtimeRoot = "";
|
||||
@@ -13,7 +13,7 @@ let getUserMasterPromptRoute: typeof import("../src/app/api/v1/master-agent/prom
|
||||
let getUserMasterMemoriesRoute: typeof import("../src/app/api/v1/master-agent/memories/route");
|
||||
let patchUserMasterMemoryRoute: typeof import("../src/app/api/v1/master-agent/memories/[memoryId]/route");
|
||||
let getProjectMemoriesRoute: typeof import("../src/app/api/v1/projects/[projectId]/memories/route");
|
||||
let getPromptProfileRoute: typeof import("../src/app/api/v1/projects/[projectId]/prompt-profile/route");
|
||||
let promptProfileRoute: typeof import("../src/app/api/v1/projects/[projectId]/prompt-profile/route");
|
||||
|
||||
async function setup() {
|
||||
if (runtimeRoot) return;
|
||||
@@ -22,7 +22,7 @@ async function setup() {
|
||||
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
|
||||
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
|
||||
|
||||
const [data, auth, promptPolicyRoute, userPromptRoute, memoriesRoute, memoryRoute, projectMemoriesRoute, promptProfileRoute] = await Promise.all([
|
||||
const [data, auth, promptPolicyRoute, userPromptRoute, memoriesRoute, memoryRoute, projectMemoriesRoute, loadedPromptProfileRoute] = await Promise.all([
|
||||
import("../src/lib/boss-data.ts"),
|
||||
import("../src/lib/boss-auth.ts"),
|
||||
import("../src/app/api/v1/master-agent/prompt-policy/route.ts"),
|
||||
@@ -40,7 +40,7 @@ async function setup() {
|
||||
getUserMasterMemoriesRoute = memoriesRoute;
|
||||
patchUserMasterMemoryRoute = memoryRoute;
|
||||
getProjectMemoriesRoute = projectMemoriesRoute.GET;
|
||||
getPromptProfileRoute = promptProfileRoute.POST;
|
||||
promptProfileRoute = loadedPromptProfileRoute;
|
||||
}
|
||||
|
||||
async function createAuthedRequest(account = "17600003315", role: "member" | "admin" | "highest_admin" = "highest_admin") {
|
||||
@@ -189,17 +189,69 @@ test("master-agent 记忆页会返回当前用户所有项目记忆", async () =
|
||||
|
||||
test("prompt-profile 写入当前对话提示词时按当前账号隔离", async () => {
|
||||
await setup();
|
||||
const tempDir = await mkdtemp(path.join(os.tmpdir(), "boss-claw-prompt-profile-"));
|
||||
const scriptPath = path.join(tempDir, "claw-runtime.mjs");
|
||||
await writeFile(scriptPath, "console.log('ok');\n", "utf8");
|
||||
const previousEnv = {
|
||||
BOSS_CLAW_ENABLED: process.env.BOSS_CLAW_ENABLED,
|
||||
BOSS_CLAW_COMMAND: process.env.BOSS_CLAW_COMMAND,
|
||||
BOSS_CLAW_ARGS: process.env.BOSS_CLAW_ARGS,
|
||||
BOSS_CLAW_WORKDIR: process.env.BOSS_CLAW_WORKDIR,
|
||||
};
|
||||
process.env.BOSS_CLAW_ENABLED = "true";
|
||||
process.env.BOSS_CLAW_COMMAND = process.execPath;
|
||||
process.env.BOSS_CLAW_ARGS = scriptPath;
|
||||
process.env.BOSS_CLAW_WORKDIR = tempDir;
|
||||
|
||||
try {
|
||||
const memberRequest = await createAuthedRequest("18800001111", "member");
|
||||
|
||||
const response = await promptProfileRoute.POST(
|
||||
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/prompt-profile", {
|
||||
method: "POST",
|
||||
headers: memberRequest.headers,
|
||||
body: JSON.stringify({
|
||||
promptOverride: "成员自己的当前对话提示词",
|
||||
backendOverride: "claw-runtime",
|
||||
}),
|
||||
}),
|
||||
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
const payload = (await response.json()) as {
|
||||
ok: boolean;
|
||||
projectPromptOverride: string | null;
|
||||
account: string;
|
||||
projectControls: {
|
||||
backendOverride?: string | null;
|
||||
} | null;
|
||||
};
|
||||
assert.equal(payload.ok, true);
|
||||
assert.equal(payload.account, "18800001111");
|
||||
assert.equal(payload.projectPromptOverride, "成员自己的当前对话提示词");
|
||||
assert.equal(payload.projectControls?.backendOverride, "claw-runtime");
|
||||
} finally {
|
||||
if (previousEnv.BOSS_CLAW_ENABLED === undefined) delete process.env.BOSS_CLAW_ENABLED;
|
||||
else process.env.BOSS_CLAW_ENABLED = previousEnv.BOSS_CLAW_ENABLED;
|
||||
if (previousEnv.BOSS_CLAW_COMMAND === undefined) delete process.env.BOSS_CLAW_COMMAND;
|
||||
else process.env.BOSS_CLAW_COMMAND = previousEnv.BOSS_CLAW_COMMAND;
|
||||
if (previousEnv.BOSS_CLAW_ARGS === undefined) delete process.env.BOSS_CLAW_ARGS;
|
||||
else process.env.BOSS_CLAW_ARGS = previousEnv.BOSS_CLAW_ARGS;
|
||||
if (previousEnv.BOSS_CLAW_WORKDIR === undefined) delete process.env.BOSS_CLAW_WORKDIR;
|
||||
else process.env.BOSS_CLAW_WORKDIR = previousEnv.BOSS_CLAW_WORKDIR;
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("prompt-profile 会返回当前 Claw Runtime 的可用性状态", async () => {
|
||||
await setup();
|
||||
|
||||
const memberRequest = await createAuthedRequest("18800001111", "member");
|
||||
|
||||
const response = await getPromptProfileRoute(
|
||||
const response = await promptProfileRoute.GET(
|
||||
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/prompt-profile", {
|
||||
method: "POST",
|
||||
method: "GET",
|
||||
headers: memberRequest.headers,
|
||||
body: JSON.stringify({
|
||||
promptOverride: "成员自己的当前对话提示词",
|
||||
backendOverride: "claw-runtime",
|
||||
}),
|
||||
}),
|
||||
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
||||
);
|
||||
@@ -207,14 +259,20 @@ test("prompt-profile 写入当前对话提示词时按当前账号隔离", async
|
||||
assert.equal(response.status, 200);
|
||||
const payload = (await response.json()) as {
|
||||
ok: boolean;
|
||||
projectPromptOverride: string | null;
|
||||
account: string;
|
||||
projectControls: {
|
||||
backendOverride?: string | null;
|
||||
} | null;
|
||||
clawAvailability?: {
|
||||
configured: boolean;
|
||||
status: string;
|
||||
selectable: boolean;
|
||||
reason: string;
|
||||
reasonLabel: string;
|
||||
};
|
||||
};
|
||||
assert.equal(payload.ok, true);
|
||||
assert.equal(payload.account, "18800001111");
|
||||
assert.equal(payload.projectPromptOverride, "成员自己的当前对话提示词");
|
||||
assert.equal(payload.projectControls?.backendOverride, "claw-runtime");
|
||||
assert.deepEqual(payload.clawAvailability, {
|
||||
configured: false,
|
||||
status: "disabled",
|
||||
selectable: false,
|
||||
reason: "disabled",
|
||||
reasonLabel: "Claw Runtime 当前未启用。",
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user