增加注入提示词功能
This commit is contained in:
parent
f193c48ce1
commit
ae059ffcb8
10 changed files with 160 additions and 20 deletions
|
|
@ -543,14 +543,17 @@ class ResponsesStreamConverter:
|
||||||
def _rewrite_top_level_model(self, payload: JsonDict) -> JsonDict:
|
def _rewrite_top_level_model(self, payload: JsonDict) -> JsonDict:
|
||||||
"""在保持上游事件结构不变的前提下回填展示模型名。
|
"""在保持上游事件结构不变的前提下回填展示模型名。
|
||||||
|
|
||||||
Responses 原生事件经常会在顶层带一个 `model` 字段;这里不重写内部结构,只把这个
|
原生 Responses 事件中 model 可能在顶层或嵌套在 response 子对象中,
|
||||||
顶层字段替换为当前映射的 Cursor 模型名,避免界面显示上游原始模型名。
|
两处都需要改写,避免界面显示上游原始模型名。
|
||||||
"""
|
"""
|
||||||
if not isinstance(payload, dict):
|
if not isinstance(payload, dict):
|
||||||
return payload
|
return payload
|
||||||
copied = dict(payload)
|
copied = dict(payload)
|
||||||
if copied.get('model'):
|
if copied.get('model'):
|
||||||
copied['model'] = self.model or copied['model']
|
copied['model'] = self.model or copied['model']
|
||||||
|
if isinstance(copied.get('response'), dict) and copied['response'].get('model'):
|
||||||
|
copied['response'] = dict(copied['response'])
|
||||||
|
copied['response']['model'] = self.model or copied['response']['model']
|
||||||
return copied
|
return copied
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -597,12 +600,13 @@ class ResponsesToCCStreamConverter:
|
||||||
def _handle_output_item_added(self, event_data: JsonDict) -> list[JsonDict]:
|
def _handle_output_item_added(self, event_data: JsonDict) -> list[JsonDict]:
|
||||||
"""处理 Responses 的 output_item.added 事件。
|
"""处理 Responses 的 output_item.added 事件。
|
||||||
|
|
||||||
这里主要关心 function_call,因为文本和 reasoning 的真正内容会分别在后续 delta
|
上游事件结构为 {type, item: {type, call_id, name, ...}, output_index},
|
||||||
事件里补全;function_call 则需要先创建一个空参数的 tool_call 槽位。
|
实际的 function_call 信息在 item 子对象中。
|
||||||
"""
|
"""
|
||||||
if event_data.get('type') != 'function_call':
|
item = event_data.get('item') or event_data
|
||||||
|
if item.get('type') != 'function_call':
|
||||||
return []
|
return []
|
||||||
call_id = event_data.get('call_id') or gen_id('call_')
|
call_id = item.get('call_id') or gen_id('call_')
|
||||||
index = self._tool_slots.setdefault(call_id, self._tool_index)
|
index = self._tool_slots.setdefault(call_id, self._tool_index)
|
||||||
if index == self._tool_index:
|
if index == self._tool_index:
|
||||||
self._tool_index += 1
|
self._tool_index += 1
|
||||||
|
|
@ -612,7 +616,7 @@ class ResponsesToCCStreamConverter:
|
||||||
'id': call_id,
|
'id': call_id,
|
||||||
'type': 'function',
|
'type': 'function',
|
||||||
'function': {
|
'function': {
|
||||||
'name': event_data.get('name', ''),
|
'name': item.get('name', ''),
|
||||||
'arguments': '',
|
'arguments': '',
|
||||||
},
|
},
|
||||||
}]
|
}]
|
||||||
|
|
@ -633,11 +637,16 @@ class ResponsesToCCStreamConverter:
|
||||||
})]
|
})]
|
||||||
|
|
||||||
def _handle_completed(self, event_data: JsonDict) -> list[JsonDict]:
|
def _handle_completed(self, event_data: JsonDict) -> list[JsonDict]:
|
||||||
"""处理 response.completed,补出聊天补全流的最终收尾 chunk。"""
|
"""处理 response.completed,补出聊天补全流的最终收尾 chunk。
|
||||||
self._usage = event_data.get('usage', {}) or {}
|
|
||||||
|
上游事件结构为 {type, response: {output, usage, ...}},
|
||||||
|
实际的 output/usage 在 response 子对象中。
|
||||||
|
"""
|
||||||
|
resp = event_data.get('response') or event_data
|
||||||
|
self._usage = resp.get('usage', {}) or {}
|
||||||
finish_reason = 'tool_calls' if any(
|
finish_reason = 'tool_calls' if any(
|
||||||
isinstance(item, dict) and item.get('type') == 'function_call'
|
isinstance(item, dict) and item.get('type') == 'function_call'
|
||||||
for item in event_data.get('output', [])
|
for item in resp.get('output', [])
|
||||||
) else 'stop'
|
) else 'stop'
|
||||||
chunk = self._make_chunk(delta={}, finish_reason=finish_reason)
|
chunk = self._make_chunk(delta={}, finish_reason=finish_reason)
|
||||||
chunk['usage'] = {
|
chunk['usage'] = {
|
||||||
|
|
@ -718,7 +727,7 @@ def _append_responses_input_item(
|
||||||
item: JsonDict = {
|
item: JsonDict = {
|
||||||
'type': 'message',
|
'type': 'message',
|
||||||
'role': role or 'user',
|
'role': role or 'user',
|
||||||
'content': _content_to_responses_parts(content),
|
'content': _content_to_responses_parts(content, role),
|
||||||
}
|
}
|
||||||
input_items.append(item)
|
input_items.append(item)
|
||||||
|
|
||||||
|
|
@ -1013,13 +1022,19 @@ def _content_to_text(content: Any) -> str:
|
||||||
return str(content) if content is not None else ''
|
return str(content) if content is not None else ''
|
||||||
|
|
||||||
|
|
||||||
def _content_to_responses_parts(content: Any) -> list[JsonDict]:
|
def _content_to_responses_parts(content: Any, role: str = 'user') -> list[JsonDict]:
|
||||||
"""将普通消息内容转换为 Responses `input_text` 数组。"""
|
"""将普通消息内容转换为 Responses 内容块数组。
|
||||||
|
|
||||||
|
assistant 消息使用 output_text,其他角色使用 input_text。
|
||||||
|
"""
|
||||||
if isinstance(content, list):
|
if isinstance(content, list):
|
||||||
text = _extract_text(content)
|
text = _extract_text(content)
|
||||||
else:
|
else:
|
||||||
text = _content_to_text(content)
|
text = _content_to_text(content)
|
||||||
return [{'type': 'input_text', 'text': text}] if text else []
|
if not text:
|
||||||
|
return []
|
||||||
|
part_type = 'output_text' if role == 'assistant' else 'input_text'
|
||||||
|
return [{'type': part_type, 'text': text}]
|
||||||
|
|
||||||
|
|
||||||
def _stringify_output(content: Any) -> str:
|
def _stringify_output(content: Any) -> str:
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,8 @@ def add_mapping():
|
||||||
'backend': data.get('backend', 'auto'),
|
'backend': data.get('backend', 'auto'),
|
||||||
'target_url': data.get('target_url', ''),
|
'target_url': data.get('target_url', ''),
|
||||||
'api_key': data.get('api_key', ''),
|
'api_key': data.get('api_key', ''),
|
||||||
|
'custom_instructions': data.get('custom_instructions', ''),
|
||||||
|
'instructions_position': data.get('instructions_position', 'prepend'),
|
||||||
}
|
}
|
||||||
return _save_and_respond(s, f'映射已添加: {name}')
|
return _save_and_respond(s, f'映射已添加: {name}')
|
||||||
|
|
||||||
|
|
@ -157,6 +159,8 @@ def update_mapping(name):
|
||||||
'backend': data.get('backend', 'auto'),
|
'backend': data.get('backend', 'auto'),
|
||||||
'target_url': data.get('target_url', ''),
|
'target_url': data.get('target_url', ''),
|
||||||
'api_key': data.get('api_key', ''),
|
'api_key': data.get('api_key', ''),
|
||||||
|
'custom_instructions': data.get('custom_instructions', ''),
|
||||||
|
'instructions_position': data.get('instructions_position', 'prepend'),
|
||||||
}
|
}
|
||||||
if new_name != name:
|
if new_name != name:
|
||||||
del mappings[name]
|
del mappings[name]
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,9 @@ from routes.common import (
|
||||||
build_responses_target,
|
build_responses_target,
|
||||||
build_route_context,
|
build_route_context,
|
||||||
chat_error_chunk,
|
chat_error_chunk,
|
||||||
|
inject_instructions_anthropic,
|
||||||
|
inject_instructions_cc,
|
||||||
|
inject_instructions_responses,
|
||||||
log_route_context,
|
log_route_context,
|
||||||
log_usage,
|
log_usage,
|
||||||
sse_data_message,
|
sse_data_message,
|
||||||
|
|
@ -108,6 +111,7 @@ def _handle_openai_backend(ctx: RouteContext, payload: dict[str, Any]):
|
||||||
)
|
)
|
||||||
|
|
||||||
payload = normalize_request(payload, ctx.upstream_model)
|
payload = normalize_request(payload, ctx.upstream_model)
|
||||||
|
payload = inject_instructions_cc(payload, ctx.custom_instructions, ctx.instructions_position)
|
||||||
_dbg(
|
_dbg(
|
||||||
f'标准化完成:模型={payload.get("model")} '
|
f'标准化完成:模型={payload.get("model")} '
|
||||||
f'工具数={len(payload.get("tools", []))}'
|
f'工具数={len(payload.get("tools", []))}'
|
||||||
|
|
@ -194,6 +198,7 @@ def _handle_responses_backend(ctx: RouteContext, payload: dict[str, Any]):
|
||||||
"""
|
"""
|
||||||
responses_payload = cc_to_responses_request(payload)
|
responses_payload = cc_to_responses_request(payload)
|
||||||
responses_payload['model'] = ctx.upstream_model
|
responses_payload['model'] = ctx.upstream_model
|
||||||
|
responses_payload = inject_instructions_responses(responses_payload, ctx.custom_instructions, ctx.instructions_position)
|
||||||
_dbg(
|
_dbg(
|
||||||
'已转换为 Responses 请求:字段=' + str(list(responses_payload.keys()))
|
'已转换为 Responses 请求:字段=' + str(list(responses_payload.keys()))
|
||||||
+ f' 输入项数={len(responses_payload.get("input", []))}'
|
+ f' 输入项数={len(responses_payload.get("input", []))}'
|
||||||
|
|
@ -270,6 +275,7 @@ def _handle_anthropic_backend(ctx: RouteContext, payload: dict[str, Any]):
|
||||||
"""处理走 Anthropic Messages 后端的聊天补全请求。"""
|
"""处理走 Anthropic Messages 后端的聊天补全请求。"""
|
||||||
payload['model'] = ctx.upstream_model
|
payload['model'] = ctx.upstream_model
|
||||||
anthropic_payload = cc_to_messages_request(payload)
|
anthropic_payload = cc_to_messages_request(payload)
|
||||||
|
anthropic_payload = inject_instructions_anthropic(anthropic_payload, ctx.custom_instructions, ctx.instructions_position)
|
||||||
_dbg(
|
_dbg(
|
||||||
'已转换为 Messages 请求:字段=' + str(list(anthropic_payload.keys()))
|
'已转换为 Messages 请求:字段=' + str(list(anthropic_payload.keys()))
|
||||||
+ f' 消息数={len(anthropic_payload.get("messages", []))}'
|
+ f' 消息数={len(anthropic_payload.get("messages", []))}'
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,8 @@ class RouteContext:
|
||||||
"""数据面路由使用的标准请求上下文。
|
"""数据面路由使用的标准请求上下文。
|
||||||
|
|
||||||
路由层会先根据客户端模型名解析出统一上下文,后续处理函数只需要关心
|
路由层会先根据客户端模型名解析出统一上下文,后续处理函数只需要关心
|
||||||
上游模型、后端类型、目标地址、鉴权信息和流式标记,而不必重复访问配置层。
|
上游模型、后端类型、目标地址、鉴权信息、流式标记和自定义指令,
|
||||||
|
而不必重复访问配置层。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
client_model: str
|
client_model: str
|
||||||
|
|
@ -31,6 +32,8 @@ class RouteContext:
|
||||||
target_url: str
|
target_url: str
|
||||||
api_key: str
|
api_key: str
|
||||||
is_stream: bool
|
is_stream: bool
|
||||||
|
custom_instructions: str
|
||||||
|
instructions_position: str
|
||||||
|
|
||||||
|
|
||||||
def build_route_context(client_model: str, is_stream: bool) -> RouteContext:
|
def build_route_context(client_model: str, is_stream: bool) -> RouteContext:
|
||||||
|
|
@ -43,6 +46,8 @@ def build_route_context(client_model: str, is_stream: bool) -> RouteContext:
|
||||||
target_url=mapping['target_url'],
|
target_url=mapping['target_url'],
|
||||||
api_key=mapping['api_key'],
|
api_key=mapping['api_key'],
|
||||||
is_stream=is_stream,
|
is_stream=is_stream,
|
||||||
|
custom_instructions=mapping.get('custom_instructions', ''),
|
||||||
|
instructions_position=mapping.get('instructions_position', 'prepend'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -120,3 +125,71 @@ def chat_error_chunk(message: str, error_type: str = 'upstream_error') -> str:
|
||||||
def responses_error_event(message: str) -> str:
|
def responses_error_event(message: str) -> str:
|
||||||
"""构造 Responses 流式接口使用的错误事件。"""
|
"""构造 Responses 流式接口使用的错误事件。"""
|
||||||
return sse_event_message('error', {'error': message})
|
return sse_event_message('error', {'error': message})
|
||||||
|
|
||||||
|
|
||||||
|
# ─── 自定义指令注入 ──────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def _merge_text(custom: str, existing: str, position: str) -> str:
|
||||||
|
"""根据 position 决定自定义指令与原有内容的拼接顺序。"""
|
||||||
|
if not existing:
|
||||||
|
return custom
|
||||||
|
if position == 'append':
|
||||||
|
return existing + '\n\n' + custom
|
||||||
|
return custom + '\n\n' + existing
|
||||||
|
|
||||||
|
|
||||||
|
def inject_instructions_cc(payload: dict[str, Any], instructions: str, position: str = 'prepend') -> dict[str, Any]:
|
||||||
|
"""向 Chat Completions 请求注入自定义指令。
|
||||||
|
|
||||||
|
position='prepend' 时放在 system 消息开头,'append' 时放在末尾。
|
||||||
|
"""
|
||||||
|
if not instructions:
|
||||||
|
return payload
|
||||||
|
|
||||||
|
messages = payload.get('messages', [])
|
||||||
|
if messages and messages[0].get('role') == 'system':
|
||||||
|
first = messages[0]
|
||||||
|
original = first.get('content') or ''
|
||||||
|
first['content'] = _merge_text(instructions, original, position)
|
||||||
|
else:
|
||||||
|
messages.insert(0, {'role': 'system', 'content': instructions})
|
||||||
|
payload['messages'] = messages
|
||||||
|
|
||||||
|
logger.info('已注入自定义指令到 CC system 消息 (%d 字符, %s)', len(instructions), position)
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def inject_instructions_responses(payload: dict[str, Any], instructions: str, position: str = 'prepend') -> dict[str, Any]:
|
||||||
|
"""向 Responses 请求注入自定义指令(写入 instructions 字段)。
|
||||||
|
|
||||||
|
position='prepend' 时放在 instructions 开头,'append' 时放在末尾。
|
||||||
|
"""
|
||||||
|
if not instructions:
|
||||||
|
return payload
|
||||||
|
|
||||||
|
existing = payload.get('instructions') or ''
|
||||||
|
payload['instructions'] = _merge_text(instructions, existing, position)
|
||||||
|
|
||||||
|
logger.info('已注入自定义指令到 Responses instructions (%d 字符, %s)', len(instructions), position)
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def inject_instructions_anthropic(payload: dict[str, Any], instructions: str, position: str = 'prepend') -> dict[str, Any]:
|
||||||
|
"""向 Anthropic Messages 请求注入自定义指令(写入 system 字段)。
|
||||||
|
|
||||||
|
position='prepend' 时放在 system 开头,'append' 时放在末尾。
|
||||||
|
"""
|
||||||
|
if not instructions:
|
||||||
|
return payload
|
||||||
|
|
||||||
|
existing = payload.get('system') or ''
|
||||||
|
if isinstance(existing, list):
|
||||||
|
existing = '\n'.join(
|
||||||
|
block.get('text', '') for block in existing
|
||||||
|
if isinstance(block, dict) and block.get('type') == 'text'
|
||||||
|
)
|
||||||
|
payload['system'] = _merge_text(instructions, existing, position)
|
||||||
|
|
||||||
|
logger.info('已注入自定义指令到 Anthropic system (%d 字符, %s)', len(instructions), position)
|
||||||
|
return payload
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ from flask import Blueprint, request, jsonify
|
||||||
|
|
||||||
import settings
|
import settings
|
||||||
from config import Config
|
from config import Config
|
||||||
|
from routes.common import inject_instructions_anthropic
|
||||||
from utils.http import build_anthropic_headers, forward_request, sse_response
|
from utils.http import build_anthropic_headers, forward_request, sse_response
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -29,11 +30,16 @@ def messages_passthrough():
|
||||||
|
|
||||||
logger.info(f'[透传] model={model} 流式={is_stream}')
|
logger.info(f'[透传] model={model} 流式={is_stream}')
|
||||||
|
|
||||||
url_base = settings.get_url()
|
mapping = settings.resolve_model(model)
|
||||||
api_key = settings.get_key()
|
url_base = mapping['target_url']
|
||||||
|
api_key = mapping['api_key']
|
||||||
|
custom_instructions = mapping.get('custom_instructions', '')
|
||||||
|
instructions_position = mapping.get('instructions_position', 'prepend')
|
||||||
headers = build_anthropic_headers(api_key)
|
headers = build_anthropic_headers(api_key)
|
||||||
url = f'{url_base.rstrip("/")}/v1/messages'
|
url = f'{url_base.rstrip("/")}/v1/messages'
|
||||||
|
|
||||||
|
payload = inject_instructions_anthropic(payload, custom_instructions, instructions_position)
|
||||||
|
|
||||||
if not is_stream:
|
if not is_stream:
|
||||||
resp, err = forward_request(url, headers, payload)
|
resp, err = forward_request(url, headers, payload)
|
||||||
if err:
|
if err:
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,9 @@ from routes.common import (
|
||||||
build_openai_target,
|
build_openai_target,
|
||||||
build_responses_target,
|
build_responses_target,
|
||||||
build_route_context,
|
build_route_context,
|
||||||
|
inject_instructions_anthropic,
|
||||||
|
inject_instructions_cc,
|
||||||
|
inject_instructions_responses,
|
||||||
log_route_context,
|
log_route_context,
|
||||||
log_usage,
|
log_usage,
|
||||||
responses_error_event,
|
responses_error_event,
|
||||||
|
|
@ -73,6 +76,7 @@ def _build_cc_payload(payload: dict[str, Any], ctx: RouteContext) -> dict[str, A
|
||||||
"""
|
"""
|
||||||
cc_payload = responses_to_cc(payload)
|
cc_payload = responses_to_cc(payload)
|
||||||
cc_payload['model'] = ctx.upstream_model
|
cc_payload['model'] = ctx.upstream_model
|
||||||
|
cc_payload = inject_instructions_cc(cc_payload, ctx.custom_instructions, ctx.instructions_position)
|
||||||
_dbg(
|
_dbg(
|
||||||
'已转换为聊天补全中间表示:字段=' + str(list(cc_payload.keys()))
|
'已转换为聊天补全中间表示:字段=' + str(list(cc_payload.keys()))
|
||||||
+ f' 消息数={len(cc_payload.get("messages", []))}'
|
+ f' 消息数={len(cc_payload.get("messages", []))}'
|
||||||
|
|
@ -171,6 +175,7 @@ def _handle_responses_backend(ctx: RouteContext, payload: dict[str, Any]):
|
||||||
"""
|
"""
|
||||||
payload = dict(payload)
|
payload = dict(payload)
|
||||||
payload['model'] = ctx.upstream_model
|
payload['model'] = ctx.upstream_model
|
||||||
|
payload = inject_instructions_responses(payload, ctx.custom_instructions, ctx.instructions_position)
|
||||||
url, headers = build_responses_target(ctx)
|
url, headers = build_responses_target(ctx)
|
||||||
|
|
||||||
if ctx.is_stream:
|
if ctx.is_stream:
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
使用 data/settings.json 存储可通过管理面板修改的设置:
|
使用 data/settings.json 存储可通过管理面板修改的设置:
|
||||||
- proxy_target_url / proxy_api_key: 可覆盖环境变量的全局配置
|
- proxy_target_url / proxy_api_key: 可覆盖环境变量的全局配置
|
||||||
- model_mappings: Cursor 模型名 → {upstream_model, backend, target_url, api_key}
|
- model_mappings: Cursor 模型名 → {upstream_model, backend, target_url, api_key, custom_instructions}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
@ -90,6 +90,8 @@ def resolve_model(model_name):
|
||||||
'backend': backend,
|
'backend': backend,
|
||||||
'target_url': m.get('target_url') or base_url,
|
'target_url': m.get('target_url') or base_url,
|
||||||
'api_key': m.get('api_key') or base_key,
|
'api_key': m.get('api_key') or base_key,
|
||||||
|
'custom_instructions': m.get('custom_instructions') or '',
|
||||||
|
'instructions_position': m.get('instructions_position') or 'prepend',
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -97,6 +99,8 @@ def resolve_model(model_name):
|
||||||
'backend': _auto_detect(model_name),
|
'backend': _auto_detect(model_name),
|
||||||
'target_url': base_url,
|
'target_url': base_url,
|
||||||
'api_key': base_key,
|
'api_key': base_key,
|
||||||
|
'custom_instructions': '',
|
||||||
|
'instructions_position': 'prepend',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,8 @@ code{background:var(--input);padding:1px 5px;border-radius:4px;font-size:12px;fo
|
||||||
.field{margin-bottom:16px}
|
.field{margin-bottom:16px}
|
||||||
.field label{display:block;font-size:13px;color:var(--muted);margin-bottom:6px;font-weight:500}
|
.field label{display:block;font-size:13px;color:var(--muted);margin-bottom:6px;font-weight:500}
|
||||||
.input-wrap{position:relative}
|
.input-wrap{position:relative}
|
||||||
.input-wrap input,.input-wrap select{width:100%;background:var(--input);border:1px solid var(--border);border-radius:8px;padding:10px 14px;color:var(--text);font-size:14px;outline:none;transition:border-color .2s}
|
.input-wrap input,.input-wrap select,.input-wrap textarea{width:100%;background:var(--input);border:1px solid var(--border);border-radius:8px;padding:10px 14px;color:var(--text);font-size:14px;outline:none;transition:border-color .2s}
|
||||||
.input-wrap input:focus,.input-wrap select:focus{border-color:var(--primary)}
|
.input-wrap input:focus,.input-wrap select:focus,.input-wrap textarea:focus{border-color:var(--primary)}
|
||||||
.input-wrap .eye{position:absolute;right:10px;top:50%;transform:translateY(-50%);background:none;border:none;color:var(--muted);cursor:pointer;padding:4px;font-size:16px}
|
.input-wrap .eye{position:absolute;right:10px;top:50%;transform:translateY(-50%);background:none;border:none;color:var(--muted);cursor:pointer;padding:4px;font-size:16px}
|
||||||
select{cursor:pointer;appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%238899ab'%3E%3Cpath d='M6 8L1 3h10z'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 12px center}
|
select{cursor:pointer;appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%238899ab'%3E%3Cpath d='M6 8L1 3h10z'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 12px center}
|
||||||
|
|
||||||
|
|
@ -61,6 +61,7 @@ main{padding:28px 0 60px}
|
||||||
.tag-responses{background:rgba(59,130,246,.15);color:#60a5fa}
|
.tag-responses{background:rgba(59,130,246,.15);color:#60a5fa}
|
||||||
.tag-auto{background:rgba(139,92,246,.15);color:#a78bfa}
|
.tag-auto{background:rgba(139,92,246,.15);color:#a78bfa}
|
||||||
.tag-override{background:rgba(59,130,246,.1);color:var(--primary)}
|
.tag-override{background:rgba(59,130,246,.1);color:var(--primary)}
|
||||||
|
.tag-instructions{background:rgba(234,179,8,.15);color:var(--yellow)}
|
||||||
.mapping-actions{margin-left:auto;display:flex;gap:6px}
|
.mapping-actions{margin-left:auto;display:flex;gap:6px}
|
||||||
.empty{text-align:center;padding:40px;color:var(--muted)}
|
.empty{text-align:center;padding:40px;color:var(--muted)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,24 @@
|
||||||
<button class="eye" onclick="togglePwd('mKey')">👁</button>
|
<button class="eye" onclick="togglePwd('mKey')">👁</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>自定义指令 <span style="color:var(--muted)">(可选)</span></label>
|
||||||
|
<div class="input-wrap"><textarea id="mInstructions" rows="5" placeholder="注入到请求中的自定义系统指令 Responses 格式 → 写入 instructions 字段(developer 级别) Chat Completions 格式 → 注入为首条 system 消息 Anthropic 格式 → 写入 system 字段" style="resize:vertical;min-height:80px"></textarea></div>
|
||||||
|
<div class="hint">
|
||||||
|
每个模型可单独配置。指令会被注入到发往上游的请求中,模型会无条件信任该内容。<br>
|
||||||
|
适用场景:自定义人格、解除限制、注入 CTF 环境假设等。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>注入位置</label>
|
||||||
|
<div class="input-wrap">
|
||||||
|
<select id="mInsPosition">
|
||||||
|
<option value="prepend">前置(推荐,优先级更高)</option>
|
||||||
|
<option value="append">后置</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="hint">前置:自定义指令放在系统提示词最前面,模型优先看到;后置:放在末尾</div>
|
||||||
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="btn btn-ghost" onclick="closeModal()">取消</button>
|
<button class="btn btn-ghost" onclick="closeModal()">取消</button>
|
||||||
<button class="btn btn-primary" id="modalSaveBtn" onclick="saveMapping()">保存</button>
|
<button class="btn btn-primary" id="modalSaveBtn" onclick="saveMapping()">保存</button>
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,7 @@ async function loadMappings() {
|
||||||
? 'responses'
|
? 'responses'
|
||||||
: backend;
|
: backend;
|
||||||
const hasOverride = m.target_url || m.api_key;
|
const hasOverride = m.target_url || m.api_key;
|
||||||
|
const hasInstructions = !!m.custom_instructions;
|
||||||
return `<div class="mapping-item">
|
return `<div class="mapping-item">
|
||||||
<div class="mapping-top">
|
<div class="mapping-top">
|
||||||
<span class="mapping-name">${esc(name)}</span>
|
<span class="mapping-name">${esc(name)}</span>
|
||||||
|
|
@ -145,6 +146,7 @@ async function loadMappings() {
|
||||||
<div class="mapping-meta">
|
<div class="mapping-meta">
|
||||||
<span class="tag ${tagClass}">${tagLabel}</span>
|
<span class="tag ${tagClass}">${tagLabel}</span>
|
||||||
${hasOverride ? '<span class="tag tag-override">自定义地址</span>' : ''}
|
${hasOverride ? '<span class="tag tag-override">自定义地址</span>' : ''}
|
||||||
|
${hasInstructions ? '<span class="tag tag-instructions">自定义指令</span>' : ''}
|
||||||
</div>
|
</div>
|
||||||
<div class="mapping-actions">
|
<div class="mapping-actions">
|
||||||
<button class="btn btn-ghost btn-sm" onclick="openEditModal('${esc(name)}')">编辑</button>
|
<button class="btn btn-ghost btn-sm" onclick="openEditModal('${esc(name)}')">编辑</button>
|
||||||
|
|
@ -167,6 +169,8 @@ function openAddModal() {
|
||||||
document.getElementById('mBackend').value = 'auto';
|
document.getElementById('mBackend').value = 'auto';
|
||||||
document.getElementById('mUrl').value = '';
|
document.getElementById('mUrl').value = '';
|
||||||
document.getElementById('mKey').value = '';
|
document.getElementById('mKey').value = '';
|
||||||
|
document.getElementById('mInstructions').value = '';
|
||||||
|
document.getElementById('mInsPosition').value = 'prepend';
|
||||||
document.getElementById('modal').classList.add('active');
|
document.getElementById('modal').classList.add('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -183,6 +187,8 @@ async function openEditModal(name) {
|
||||||
document.getElementById('mBackend').value = m.backend || 'auto';
|
document.getElementById('mBackend').value = m.backend || 'auto';
|
||||||
document.getElementById('mUrl').value = m.target_url || '';
|
document.getElementById('mUrl').value = m.target_url || '';
|
||||||
document.getElementById('mKey').value = m.api_key || '';
|
document.getElementById('mKey').value = m.api_key || '';
|
||||||
|
document.getElementById('mInstructions').value = m.custom_instructions || '';
|
||||||
|
document.getElementById('mInsPosition').value = m.instructions_position || 'prepend';
|
||||||
document.getElementById('modal').classList.add('active');
|
document.getElementById('modal').classList.add('active');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast('错误: ' + e.message, false);
|
toast('错误: ' + e.message, false);
|
||||||
|
|
@ -206,6 +212,8 @@ async function saveMapping() {
|
||||||
backend: document.getElementById('mBackend').value,
|
backend: document.getElementById('mBackend').value,
|
||||||
target_url: document.getElementById('mUrl').value.trim(),
|
target_url: document.getElementById('mUrl').value.trim(),
|
||||||
api_key: document.getElementById('mKey').value.trim(),
|
api_key: document.getElementById('mKey').value.trim(),
|
||||||
|
custom_instructions: document.getElementById('mInstructions').value,
|
||||||
|
instructions_position: document.getElementById('mInsPosition').value,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue