Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions app/core/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,20 @@ def json_dumps(obj: Any) -> str:
def json_loads(obj: str | bytes) -> Any:
return orjson.loads(obj)


def _toml_escape_string(value: str) -> str:
"""Escape a string so it is safe in a single-line TOML basic string."""
return (
value
.replace("\\", "\\\\")
.replace('"', '\\"')
.replace("\b", "\\b")
.replace("\f", "\\f")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t")
)

class StorageError(Exception):
"""存储服务基础异常"""
pass
Expand Down Expand Up @@ -169,14 +183,14 @@ async def save_config(self, data: Dict[str, Any]):
if isinstance(val, bool):
val_str = "true" if val else "false"
elif isinstance(val, str):
escaped = val.replace('"', '\\"')
escaped = _toml_escape_string(val)
val_str = f'"{escaped}"'
elif isinstance(val, (int, float)):
val_str = str(val)
elif isinstance(val, (list, dict)):
val_str = json_dumps(val)
else:
val_str = f'"{str(val)}"'
val_str = f'"{_toml_escape_string(str(val))}"'
lines.append(f"{key} = {val_str}")
lines.append("")

Expand Down
19 changes: 19 additions & 0 deletions app/services/grok/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@
BROWSER = "chrome136"


def _read_custom_personality() -> str:
"""Read configured custom personality text and keep original formatting."""
value = get_config("grok.custom_personality", "")
if value is None:
return ""
text = str(value)
return text if text.strip() else ""


@dataclass
class ChatRequest:
"""聊天请求数据"""
Expand Down Expand Up @@ -256,6 +265,15 @@ def build_payload(
}
}

@staticmethod
def apply_custom_personality(payload: Dict[str, Any]) -> Dict[str, Any]:
"""Inject optional customPersonality into upstream payload."""
custom_personality = _read_custom_personality()
if custom_personality:
payload["customPersonality"] = custom_personality
else:
payload.pop("customPersonality", None)
return payload

# ==================== Grok 服务 ====================

Expand Down Expand Up @@ -300,6 +318,7 @@ async def chat(
message, model, mode, think,
file_attachments, image_attachments
)
payload = ChatRequestBuilder.apply_custom_personality(payload)
proxies = {"http": self.proxy, "https": self.proxy} if self.proxy else None
timeout = get_config("grok.timeout", TIMEOUT)

Expand Down
1 change: 1 addition & 0 deletions app/services/grok/imagine_experimental.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,7 @@ async def chat_edit(

last_error: Optional[Exception] = None
for payload in payloads:
payload = ChatRequestBuilder.apply_custom_personality(payload)
session = AsyncSession(impersonate=BROWSER)
response = None
try:
Expand Down
3 changes: 3 additions & 0 deletions app/services/grok/media.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from app.services.token import get_token_manager
from app.services.grok.processor import VideoStreamProcessor, VideoCollectProcessor
from app.services.request_stats import request_stats
from app.services.grok.chat import ChatRequestBuilder

# API 端点
CREATE_POST_API = "https://grok.com/rest/media/post/create"
Expand Down Expand Up @@ -243,6 +244,7 @@ async def generate(
# Step 2: 建立连接
headers = self._build_headers(token)
payload = self._build_payload(prompt, post_id, aspect_ratio, video_length, resolution, preset)
payload = ChatRequestBuilder.apply_custom_personality(payload)

session = AsyncSession(impersonate=BROWSER)
response = await session.post(
Expand Down Expand Up @@ -323,6 +325,7 @@ async def generate_from_image(
# Step 2: 建立连接
headers = self._build_headers(token)
payload = self._build_payload(prompt, post_id, aspect_ratio, video_length, resolution, preset)
payload = ChatRequestBuilder.apply_custom_personality(payload)

session = AsyncSession(impersonate=BROWSER)
response = await session.post(
Expand Down
10 changes: 10 additions & 0 deletions app/static/config/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const LOCALE_MAP = {
"temporary": { title: "临时对话", desc: "是否启用临时对话模式。" },
"stream": { title: "流式响应", desc: "是否默认启用流式输出。" },
"thinking": { title: "思维链", desc: "是否启用模型思维链输出。" },
"custom_personality": { title: "自定义指令", desc: "附加到所有 Grok 聊天请求的自定义指令内容(支持多行)。" },
"dynamic_statsig": { title: "动态指纹", desc: "是否启用动态生成 Statsig 值。" },
"filter_tags": { title: "过滤标签", desc: "自动过滤 Grok 响应中的特殊标签。" },
"video_poster_preview": { title: "视频海报预览", desc: "启用后会将返回内容中的 <video> 标签替换为带播放按钮的 Poster 预览图;点击预览图会在新标签页打开视频(默认关闭)。" },
Expand Down Expand Up @@ -274,6 +275,15 @@ function renderConfig(data) {
input.dataset.type = 'json';
inputWrapper.appendChild(input);
}
else if (key === 'custom_personality') {
input = document.createElement('textarea');
input.className = 'geist-input';
input.rows = 5;
input.value = typeof val === 'string' ? val : '';
input.dataset.section = section;
input.dataset.key = key;
inputWrapper.appendChild(input);
}
else {
input = document.createElement('input');
input.type = 'text';
Expand Down
1 change: 1 addition & 0 deletions config.defaults.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ timeout = 120
base_proxy_url = ""
asset_proxy_url = ""
cf_clearance = ""
custom_personality = ""
max_retry = 3
retry_status_codes = [401,429,403]
image_generation_method = "legacy"
Expand Down
9 changes: 8 additions & 1 deletion src/grok/conversation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,14 @@ export async function sendConversationRequest(args: {
settings: GrokSettings;
referer?: string;
}): Promise<Response> {
const { payload, cookie, settings, referer } = args;
const { cookie, settings, referer } = args;
const payload: Record<string, unknown> = { ...args.payload };
const customPersonality = String(settings.custom_personality ?? "");
if (customPersonality.trim()) {
payload.customPersonality = customPersonality;
} else {
delete payload.customPersonality;
}
const headers = getDynamicHeaders(settings, "/rest/app-chat/conversations/new");
headers.Cookie = cookie;
if (referer) headers.Referer = referer;
Expand Down
3 changes: 3 additions & 0 deletions src/routes/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ adminRoutes.get("/api/v1/admin/config", requireAdminAuth, async (c) => {
temporary: Boolean(settings.grok.temporary),
stream: true,
thinking: Boolean(settings.grok.show_thinking),
custom_personality: String(settings.grok.custom_personality ?? ""),
dynamic_statsig: Boolean(settings.grok.dynamic_statsig),
filter_tags: filterTags,
video_poster_preview: Boolean(settings.grok.video_poster_preview),
Expand Down Expand Up @@ -327,6 +328,8 @@ adminRoutes.post("/api/v1/admin/config", requireAdminAuth, async (c) => {
if (typeof grokCfg.base_proxy_url === "string") grok_config.proxy_url = grokCfg.base_proxy_url.trim();
if (typeof grokCfg.asset_proxy_url === "string") grok_config.cache_proxy_url = grokCfg.asset_proxy_url.trim();
if (typeof grokCfg.cf_clearance === "string") grok_config.cf_clearance = grokCfg.cf_clearance.trim();
if (typeof grokCfg.custom_personality === "string")
grok_config.custom_personality = grokCfg.custom_personality;
if (typeof grokCfg.filter_tags === "string") {
grok_config.filtered_tags = grokCfg.filter_tags;
} else if (Array.isArray(grokCfg.filter_tags)) {
Expand Down
2 changes: 2 additions & 0 deletions src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface GrokSettings {
proxy_pool_interval?: number;
cache_proxy_url?: string;
cf_clearance?: string; // stored as VALUE only (no "cf_clearance=" prefix)
custom_personality?: string;
x_statsig_id?: string;
dynamic_statsig?: boolean;
filtered_tags?: string;
Expand Down Expand Up @@ -96,6 +97,7 @@ const DEFAULTS: SettingsBundle = {
proxy_pool_interval: 300,
cache_proxy_url: "",
cf_clearance: "",
custom_personality: "",
x_statsig_id: "",
dynamic_statsig: true,
filtered_tags: "xaiartifact,xai:tool_usage_card",
Expand Down