();
+const autoApprovalTitle = computed(() => props.autoApprovalTitle || '自动审批记录');
+const isGoalApprovalMode = computed(() => autoApprovalTitle.value === '目标审批');
+const panelTitle = computed(() =>
+ isGoalApprovalMode.value ? '目标审批' : `工具审批 (${props.approvals.length})`
+);
+
const emit = defineEmits<{
(event: 'approve', approvalId: string): void;
(event: 'reject', approvalId: string): void;
@@ -239,9 +246,9 @@ const parseFinalMessage = (text: string) => {
.auto-approval-block {
margin-top: 12px;
padding: 10px;
- border: 1px solid var(--border-color, #2a2f3a);
+ border: 1px solid var(--theme-control-border, var(--border-color, #2a2f3a));
border-radius: 8px;
- background: var(--panel-bg, rgba(0, 0, 0, 0.12));
+ background: var(--theme-surface-muted, var(--panel-bg, rgba(0, 0, 0, 0.12)));
}
.auto-approval-block__title {
@@ -281,4 +288,14 @@ const parseFinalMessage = (text: string) => {
white-space: pre-wrap;
word-break: break-word;
}
+:global(:root[data-theme='dark']) .auto-approval-block,
+:global(body[data-theme='dark']) .auto-approval-block {
+ background: color-mix(in srgb, var(--theme-surface-muted, #141414) 86%, white 4%);
+ border-color: var(--theme-control-border-strong, rgba(255, 255, 255, 0.12));
+ color: var(--claude-text, #ffffff);
+}
+:global(:root[data-theme='dark']) .auto-approval-block__content,
+:global(body[data-theme='dark']) .auto-approval-block__content {
+ color: var(--claude-text-secondary, #a0a0a0);
+}
diff --git a/static/src/components/personalization/PersonalizationDrawer.vue b/static/src/components/personalization/PersonalizationDrawer.vue
index 8dd5874..ac22ece 100644
--- a/static/src/components/personalization/PersonalizationDrawer.vue
+++ b/static/src/components/personalization/PersonalizationDrawer.vue
@@ -704,6 +704,95 @@
class="fancy-path"
>
+
+
+ 目标模式
+
+
+
+
+
+ 最大自动续命轮数目标模式下,主模型停下后最多自动继续的轮数(1-100)。
+
+
+
+
+
+
+ 累计 token 上限达到该累计消耗即停止目标模式。
+
+
typeof form.value.goal_max_tokens === 'number' && form.value.goal_max_tokens > 0
+);
+
+const clampGoalMaxTurns = (raw: any): number => {
+ const n = Math.round(Number(raw));
+ if (!Number.isFinite(n)) return 5;
+ return Math.min(100, Math.max(1, n));
+};
+
+const clampGoalMaxTokens = (raw: any): number => {
+ const n = Math.round(Number(raw));
+ if (!Number.isFinite(n)) return 100000;
+ return Math.min(100000000, Math.max(1000, n));
+};
+
+const toggleGoalTokenLimit = (enabled: boolean) => {
+ personalization.updateField({
+ key: 'goal_max_tokens',
+ value: enabled ? form.value.goal_max_tokens || 100000 : null
+ });
+};
+
type IconKey = keyof typeof ICONS;
type PersonalTab =
@@ -2465,6 +2578,7 @@ const applyThemeOption = async (theme: ThemeKey) => {
.settings-input-row input,
.settings-add-row input,
.settings-number-row input,
+.settings-number-input,
.settings-compression-grid input {
height: 38px;
border: 1px solid var(--theme-control-border, var(--claude-border));
@@ -2476,6 +2590,30 @@ const applyThemeOption = async (theme: ThemeKey) => {
font-size: 13px;
}
+.settings-number-input {
+ width: 120px;
+ text-align: right;
+}
+
+.settings-section-divider {
+ display: flex;
+ align-items: center;
+ margin: 18px 0 6px;
+}
+.settings-section-divider__label {
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--claude-text-secondary, #7f7766);
+ letter-spacing: 0.04em;
+}
+.settings-section-divider::after {
+ content: '';
+ flex: 1;
+ height: 1px;
+ margin-left: 10px;
+ background: var(--theme-control-border, var(--claude-border));
+}
+
.settings-input-row > input {
width: min(360px, 40vw);
text-align: right;
@@ -2484,6 +2622,7 @@ const applyThemeOption = async (theme: ThemeKey) => {
.settings-input-row input:focus,
.settings-add-row input:focus,
.settings-number-row input:focus,
+.settings-number-input:focus,
.settings-compression-grid input:focus {
border-color: var(--claude-text-secondary);
box-shadow: none;
diff --git a/static/src/stores/input.ts b/static/src/stores/input.ts
index 2eabdf0..a86e3e7 100644
--- a/static/src/stores/input.ts
+++ b/static/src/stores/input.ts
@@ -12,6 +12,11 @@ interface InputState {
selectedImages: string[];
videoPickerOpen: boolean;
selectedVideos: string[];
+ // 目标模式(Goal Mode)
+ goalModeArmed: boolean; // 已就绪:下一条消息作为目标发送
+ goalRunning: boolean; // 运行中:目标循环正在进行
+ goalProgress: Record | null; // 最新进度快照(轮数/token/工具次数/用时/总结等)
+ goalDialogOpen: boolean; // 进度/完成弹窗
}
export const useInputStore = defineStore('input', {
@@ -26,7 +31,11 @@ export const useInputStore = defineStore('input', {
imagePickerOpen: false,
selectedImages: [],
videoPickerOpen: false,
- selectedVideos: []
+ selectedVideos: [],
+ goalModeArmed: false,
+ goalRunning: false,
+ goalProgress: null,
+ goalDialogOpen: false
}),
actions: {
setInputMessage(value: string) {
@@ -110,6 +119,28 @@ export const useInputStore = defineStore('input', {
},
clearSelectedVideos() {
this.selectedVideos = [];
+ },
+ // ---- 目标模式 ----
+ toggleGoalArmed() {
+ // 运行中不允许通过开关切换
+ if (this.goalRunning) return this.goalModeArmed;
+ this.goalModeArmed = !this.goalModeArmed;
+ return this.goalModeArmed;
+ },
+ setGoalArmed(armed: boolean) {
+ this.goalModeArmed = armed;
+ },
+ setGoalRunning(running: boolean) {
+ this.goalRunning = running;
+ if (running) {
+ this.goalModeArmed = false;
+ }
+ },
+ setGoalProgress(progress: Record | null) {
+ this.goalProgress = progress;
+ },
+ setGoalDialogOpen(open: boolean) {
+ this.goalDialogOpen = open;
}
}
});
diff --git a/static/src/stores/personalization.ts b/static/src/stores/personalization.ts
index b4d58fa..32c673e 100644
--- a/static/src/stores/personalization.ts
+++ b/static/src/stores/personalization.ts
@@ -45,6 +45,10 @@ interface PersonalForm {
agents_md_auto_inject: boolean;
allow_root_file_creation: boolean;
theme: 'classic' | 'light' | 'dark';
+ goal_review_mode: 'readonly' | 'active';
+ goal_end_conditions: string[];
+ goal_max_turns: number;
+ goal_max_tokens: number | null;
}
interface ExperimentState {
@@ -130,7 +134,11 @@ const defaultForm = (): PersonalForm => ({
deep_compress_trigger_tokens: null,
agents_md_auto_inject: false,
allow_root_file_creation: false,
- theme: loadCachedTheme()
+ theme: loadCachedTheme(),
+ goal_review_mode: 'readonly',
+ goal_end_conditions: ['max_turns'],
+ goal_max_turns: 5,
+ goal_max_tokens: null
});
const defaultExperimentState = (): ExperimentState => ({
@@ -320,7 +328,21 @@ export const usePersonalizationStore = defineStore('personalization', {
),
agents_md_auto_inject: !!data.agents_md_auto_inject,
allow_root_file_creation: !!data.allow_root_file_creation,
- theme: ['classic', 'light', 'dark'].includes(data.theme) ? data.theme : fallbackTheme
+ theme: ['classic', 'light', 'dark'].includes(data.theme) ? data.theme : fallbackTheme,
+ goal_review_mode: data.goal_review_mode === 'active' ? 'active' : 'readonly',
+ goal_end_conditions: Array.isArray(data.goal_end_conditions)
+ ? data.goal_end_conditions.filter(
+ (x: any) => x === 'max_turns' || x === 'max_tokens'
+ )
+ : ['max_turns'],
+ goal_max_turns:
+ typeof data.goal_max_turns === 'number' && data.goal_max_turns > 0
+ ? data.goal_max_turns
+ : 5,
+ goal_max_tokens:
+ typeof data.goal_max_tokens === 'number' && data.goal_max_tokens > 0
+ ? data.goal_max_tokens
+ : null
};
// 如果theme发生变化,应用到界面
const currentTheme = (typeof window !== 'undefined' && window.localStorage)
diff --git a/static/src/stores/task.ts b/static/src/stores/task.ts
index 23ebfe3..582ede6 100644
--- a/static/src/stores/task.ts
+++ b/static/src/stores/task.ts
@@ -69,6 +69,7 @@ export const useTaskStore = defineStore('task', {
run_mode?: 'fast' | 'thinking' | 'deep' | null;
thinking_mode?: boolean | null;
message_source?: string | null;
+ goal_mode?: boolean | null;
eventHandler?: (event: any) => void;
} = {}
) {
@@ -89,7 +90,8 @@ export const useTaskStore = defineStore('task', {
run_mode: options.run_mode ?? undefined,
thinking_mode:
typeof options.thinking_mode === 'boolean' ? options.thinking_mode : undefined,
- message_source: options.message_source ?? undefined
+ message_source: options.message_source ?? undefined,
+ goal_mode: options.goal_mode === true ? true : undefined
})
});
diff --git a/utils/api_client.py b/utils/api_client.py
index cb72ea8..e6be80a 100644
--- a/utils/api_client.py
+++ b/utils/api_client.py
@@ -410,6 +410,44 @@ class DeepSeekClient: # legacy name; generic OpenAI-compatible client
}
)
return sanitized
+
+ def _sanitize_message_fields_for_api(self, messages: List[Dict]) -> List[Dict]:
+ """移除只供本地 UI/持久化使用、OpenAI-compatible API 不接受的消息字段。"""
+ sanitized: List[Dict[str, Any]] = []
+ stripped_fields: Dict[str, int] = {}
+
+ allowed_common = {"role", "content", "name"}
+ allowed_by_role = {
+ "assistant": allowed_common | {"tool_calls", "reasoning_content"},
+ "tool": allowed_common | {"tool_call_id"},
+ "user": allowed_common,
+ "system": allowed_common,
+ }
+
+ for message in messages or []:
+ if not isinstance(message, dict):
+ continue
+ role = str(message.get("role") or "").strip()
+ allowed = allowed_by_role.get(role, allowed_common)
+ clean: Dict[str, Any] = {}
+ for key, value in message.items():
+ if key not in allowed:
+ stripped_fields[key] = stripped_fields.get(key, 0) + 1
+ continue
+ if key == "tool_calls" and not value:
+ stripped_fields[key] = stripped_fields.get(key, 0) + 1
+ continue
+ clean[key] = value
+ sanitized.append(clean)
+
+ if stripped_fields:
+ self._debug_log(
+ {
+ "event": "sanitize_message_fields_for_api",
+ "stripped_fields": stripped_fields,
+ }
+ )
+ return sanitized
def set_deep_thinking_mode(self, enabled: bool):
"""配置深度思考模式(持续使用思考模型)。"""
@@ -689,6 +727,7 @@ class DeepSeekClient: # legacy name; generic OpenAI-compatible client
final_messages = self._merge_system_messages(messages)
final_messages = self._sanitize_messages_for_model_capability(final_messages)
+ final_messages = self._sanitize_message_fields_for_api(final_messages)
payload = {
"model": api_config["model_id"],