mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-09 04:40:57 +00:00
Merge remote-tracking branch 'origin/master' into deploy/prod-workspace-debug
This commit is contained in:
@@ -14,8 +14,8 @@ services:
|
|||||||
restart: on-failure
|
restart: on-failure
|
||||||
environment:
|
environment:
|
||||||
- TZ=Asia/Shanghai
|
- TZ=Asia/Shanghai
|
||||||
# Shared with the langbot service and sent only as a WebSocket handshake
|
# Optional. Leave unset on both OSS services, or set the same value on
|
||||||
# header. Generate with: openssl rand -hex 32
|
# both to protect the control WebSocket. Generate with: openssl rand -hex 32
|
||||||
- LANGBOT_PLUGIN_RUNTIME_CONTROL_TOKEN=${LANGBOT_PLUGIN_RUNTIME_CONTROL_TOKEN:-}
|
- LANGBOT_PLUGIN_RUNTIME_CONTROL_TOKEN=${LANGBOT_PLUGIN_RUNTIME_CONTROL_TOKEN:-}
|
||||||
# Process-wide admission for every asyncio.to_thread() call.
|
# Process-wide admission for every asyncio.to_thread() call.
|
||||||
- LANGBOT_BLOCKING_EXECUTOR_MAX_WORKERS=${LANGBOT_BLOCKING_EXECUTOR_MAX_WORKERS:-8}
|
- LANGBOT_BLOCKING_EXECUTOR_MAX_WORKERS=${LANGBOT_BLOCKING_EXECUTOR_MAX_WORKERS:-8}
|
||||||
@@ -77,8 +77,7 @@ services:
|
|||||||
restart: on-failure
|
restart: on-failure
|
||||||
environment:
|
environment:
|
||||||
- TZ=Asia/Shanghai
|
- TZ=Asia/Shanghai
|
||||||
# Must match langbot_plugin_runtime. Empty/missing values make the
|
# Optional. Leave unset on both OSS services, or match plugin Runtime.
|
||||||
# external control channel fail closed.
|
|
||||||
- LANGBOT_PLUGIN_RUNTIME_CONTROL_TOKEN=${LANGBOT_PLUGIN_RUNTIME_CONTROL_TOKEN:-}
|
- LANGBOT_PLUGIN_RUNTIME_CONTROL_TOKEN=${LANGBOT_PLUGIN_RUNTIME_CONTROL_TOKEN:-}
|
||||||
# Must match the value supplied to langbot_box. The token is sent only
|
# Must match the value supplied to langbot_box. The token is sent only
|
||||||
# in WebSocket handshake headers, never in URLs or action payloads.
|
# in WebSocket handshake headers, never in URLs or action payloads.
|
||||||
|
|||||||
+1
-1
@@ -71,7 +71,7 @@ dependencies = [
|
|||||||
"chromadb>=1.0.0,<2.0.0",
|
"chromadb>=1.0.0,<2.0.0",
|
||||||
"qdrant-client (>=1.15.1,<2.0.0)",
|
"qdrant-client (>=1.15.1,<2.0.0)",
|
||||||
"pyseekdb==1.1.0.post3",
|
"pyseekdb==1.1.0.post3",
|
||||||
"langbot-plugin @ git+https://github.com/langbot-app/langbot-plugin-sdk.git@101e453e916b39465a6294d6471c9eaae8725d5c",
|
"langbot-plugin @ git+https://github.com/langbot-app/langbot-plugin-sdk.git@22e470d853e00116ad4fa63d3171f88646b06153",
|
||||||
"asyncpg>=0.30.0",
|
"asyncpg>=0.30.0",
|
||||||
"line-bot-sdk>=3.19.0",
|
"line-bot-sdk>=3.19.0",
|
||||||
"matrix-nio>=0.25.2",
|
"matrix-nio>=0.25.2",
|
||||||
|
|||||||
@@ -32,12 +32,13 @@ The `all` / `box` profile starts three services:
|
|||||||
the LangBot and Box containers. Generate it once with `openssl rand -hex 32`;
|
the LangBot and Box containers. Generate it once with `openssl rand -hex 32`;
|
||||||
never put it in `box.runtime.endpoint` or commit it to config.
|
never put it in `box.runtime.endpoint` or commit it to config.
|
||||||
|
|
||||||
Every Compose deployment also needs one
|
A Compose deployment may optionally set
|
||||||
`LANGBOT_PLUGIN_RUNTIME_CONTROL_TOKEN` shared by `langbot` and
|
`LANGBOT_PLUGIN_RUNTIME_CONTROL_TOKEN` on both `langbot` and
|
||||||
`langbot_plugin_runtime`. Generate it with `openssl rand -hex 32` and export it
|
`langbot_plugin_runtime` when port 5400 needs shared-secret protection. OSS
|
||||||
before `docker compose up`; the external Plugin Runtime fails closed when the
|
defaults to leaving it unset on both sides. If enabled, generate one value with
|
||||||
token is empty or weak. Kubernetes uses the `langbot-plugin-runtime-control`
|
`openssl rand -hex 32`; configuring only one side causes the control connection
|
||||||
Secret shown in `docker/kubernetes.yaml`.
|
to fail. Kubernetes may use the `langbot-plugin-runtime-control` Secret shown in
|
||||||
|
`docker/kubernetes.yaml`.
|
||||||
|
|
||||||
With Box off, the dashboard/skills list stays visible (read-only) but sandbox
|
With Box off, the dashboard/skills list stays visible (read-only) but sandbox
|
||||||
tools, skill add/edit, and stdio MCP are disabled. Set `box.enabled: false`
|
tools, skill add/edit, and stdio MCP are disabled. Set `box.enabled: false`
|
||||||
|
|||||||
@@ -392,8 +392,8 @@ class PluginsRouterGroup(group.RouterGroup):
|
|||||||
)
|
)
|
||||||
async def _(request_context: RequestContext) -> str:
|
async def _(request_context: RequestContext) -> str:
|
||||||
"""Get plugin debug information including debug URL and key"""
|
"""Get plugin debug information including debug URL and key"""
|
||||||
await self._require_authenticated_plugin_runtime_context(request_context)
|
execution_context = await self._require_authenticated_plugin_runtime_context(request_context)
|
||||||
debug_info = await self.ap.plugin_connector.get_debug_info()
|
debug_info = await self.ap.plugin_connector.get_debug_info(execution_context)
|
||||||
|
|
||||||
# Get debug URL from config
|
# Get debug URL from config
|
||||||
plugin_config = self.ap.instance_config.data.get('plugin', {})
|
plugin_config = self.ap.instance_config.data.get('plugin', {})
|
||||||
@@ -403,6 +403,7 @@ class PluginsRouterGroup(group.RouterGroup):
|
|||||||
data={
|
data={
|
||||||
'debug_url': debug_url,
|
'debug_url': debug_url,
|
||||||
'plugin_debug_key': debug_info.get('plugin_debug_key', ''),
|
'plugin_debug_key': debug_info.get('plugin_debug_key', ''),
|
||||||
|
'expires_at': debug_info.get('expires_at', ''),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -251,6 +251,8 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
|
|||||||
def _control_headers(self, *, allow_generate: bool) -> dict[str, str]:
|
def _control_headers(self, *, allow_generate: bool) -> dict[str, str]:
|
||||||
if not self._control_token and allow_generate:
|
if not self._control_token and allow_generate:
|
||||||
self._control_token = secrets.token_urlsafe(48)
|
self._control_token = secrets.token_urlsafe(48)
|
||||||
|
if not self._control_token:
|
||||||
|
return {}
|
||||||
try:
|
try:
|
||||||
self._control_token = validate_runtime_secret(
|
self._control_token = validate_runtime_secret(
|
||||||
self._control_token,
|
self._control_token,
|
||||||
@@ -1968,11 +1970,11 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
|
|||||||
with runtime_handler.installation_scope(binding):
|
with runtime_handler.installation_scope(binding):
|
||||||
return await runtime_handler.handle_page_api(plugin_author, plugin_name, page_id, endpoint, method, body)
|
return await runtime_handler.handle_page_api(plugin_author, plugin_name, page_id, endpoint, method, body)
|
||||||
|
|
||||||
async def get_debug_info(self) -> dict[str, Any]:
|
async def get_debug_info(self, execution_context: ExecutionContext) -> dict[str, Any]:
|
||||||
"""Get debug information including debug key and WS URL"""
|
"""Get debug information including debug key and WS URL"""
|
||||||
if not self.is_enable_plugin or not self._runtime_available():
|
if not self.is_enable_plugin or not self._runtime_available():
|
||||||
return {}
|
return {}
|
||||||
return await self._runtime_handler().get_debug_info()
|
return await self._runtime_handler().get_debug_info(execution_context)
|
||||||
|
|
||||||
async def emit_event(
|
async def emit_event(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -1960,14 +1960,14 @@ class RuntimeConnectionHandler(handler.Handler):
|
|||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
async def get_debug_info(self) -> dict[str, Any]:
|
async def get_debug_info(self, execution_context: ExecutionContext) -> dict[str, Any]:
|
||||||
"""Get debug information including debug key and WS URL"""
|
"""Get debug information including debug key and WS URL"""
|
||||||
with self.installation_scope(None):
|
result = await self.call_action(
|
||||||
result = await self.call_action(
|
LangBotToRuntimeAction.GET_DEBUG_INFO,
|
||||||
LangBotToRuntimeAction.GET_DEBUG_INFO,
|
{},
|
||||||
{},
|
timeout=10,
|
||||||
timeout=10,
|
action_context=execution_context,
|
||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
# ================= RAG Capability Callers (LangBot -> Runtime) =================
|
# ================= RAG Capability Callers (LangBot -> Runtime) =================
|
||||||
|
|||||||
@@ -106,7 +106,12 @@ async def plugin_security_api(plugin_module):
|
|||||||
application.plugin_connector.require_workspace_context = AsyncMock()
|
application.plugin_connector.require_workspace_context = AsyncMock()
|
||||||
application.plugin_connector.list_plugins = AsyncMock(return_value=[raw_plugin])
|
application.plugin_connector.list_plugins = AsyncMock(return_value=[raw_plugin])
|
||||||
application.plugin_connector.get_plugin_info = AsyncMock(return_value=raw_plugin)
|
application.plugin_connector.get_plugin_info = AsyncMock(return_value=raw_plugin)
|
||||||
application.plugin_connector.get_debug_info = AsyncMock(return_value={'plugin_debug_key': 'runtime-debug-secret'})
|
application.plugin_connector.get_debug_info = AsyncMock(
|
||||||
|
return_value={
|
||||||
|
'plugin_debug_key': 'runtime-debug-secret',
|
||||||
|
'expires_at': '2026-08-04T12:00:00Z',
|
||||||
|
}
|
||||||
|
)
|
||||||
application.plugin_connector.get_plugin_logs = AsyncMock(return_value=['private runtime line'])
|
application.plugin_connector.get_plugin_logs = AsyncMock(return_value=['private runtime line'])
|
||||||
application.plugin_connector.set_plugin_config = AsyncMock()
|
application.plugin_connector.set_plugin_config = AsyncMock()
|
||||||
|
|
||||||
@@ -232,8 +237,9 @@ async def test_debug_key_requires_resource_manage_permission(plugin_security_api
|
|||||||
assert (await allowed.get_json())['data'] == {
|
assert (await allowed.get_json())['data'] == {
|
||||||
'debug_url': 'http://localhost:5401',
|
'debug_url': 'http://localhost:5401',
|
||||||
'plugin_debug_key': 'runtime-debug-secret',
|
'plugin_debug_key': 'runtime-debug-secret',
|
||||||
|
'expires_at': '2026-08-04T12:00:00Z',
|
||||||
}
|
}
|
||||||
application.plugin_connector.get_debug_info.assert_awaited_once_with()
|
application.plugin_connector.get_debug_info.assert_awaited_once()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
@@ -612,8 +612,13 @@ class TestDisabledPluginEarlyReturns:
|
|||||||
mock_app.instance_config.data = {'plugin': {'enable': False}}
|
mock_app.instance_config.data = {'plugin': {'enable': False}}
|
||||||
|
|
||||||
connector = connector_module.PluginRuntimeConnector(mock_app, mock_disconnect)
|
connector = connector_module.PluginRuntimeConnector(mock_app, mock_disconnect)
|
||||||
|
execution_context = connector_module.ExecutionContext(
|
||||||
|
instance_uuid='instance-a',
|
||||||
|
workspace_uuid='workspace-a',
|
||||||
|
placement_generation=1,
|
||||||
|
)
|
||||||
|
|
||||||
result = await connector.get_debug_info()
|
result = await connector.get_debug_info(execution_context)
|
||||||
|
|
||||||
assert result == {}
|
assert result == {}
|
||||||
|
|
||||||
|
|||||||
@@ -282,12 +282,11 @@ def test_closed_deployment_selects_instance_scoped_shared_profile():
|
|||||||
assert connector.runtime_profile == 'shared'
|
assert connector.runtime_profile == 'shared'
|
||||||
|
|
||||||
|
|
||||||
def test_external_runtime_control_headers_require_strong_secret(monkeypatch):
|
def test_external_runtime_control_headers_are_empty_when_secret_is_unset(monkeypatch):
|
||||||
monkeypatch.delenv(PLUGIN_RUNTIME_CONTROL_TOKEN_ENV, raising=False)
|
monkeypatch.delenv(PLUGIN_RUNTIME_CONTROL_TOKEN_ENV, raising=False)
|
||||||
connector = make_connector()
|
connector = make_connector()
|
||||||
|
|
||||||
with pytest.raises(PluginRuntimeNotConnectedError, match=PLUGIN_RUNTIME_CONTROL_TOKEN_ENV):
|
assert connector._control_headers(allow_generate=False) == {}
|
||||||
connector._control_headers(allow_generate=False)
|
|
||||||
|
|
||||||
|
|
||||||
def test_local_runtime_control_headers_generate_ephemeral_secret(monkeypatch):
|
def test_local_runtime_control_headers_generate_ephemeral_secret(monkeypatch):
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ function PluginListView() {
|
|||||||
const [debugInfo, setDebugInfo] = useState<{
|
const [debugInfo, setDebugInfo] = useState<{
|
||||||
debug_url: string;
|
debug_url: string;
|
||||||
plugin_debug_key: string;
|
plugin_debug_key: string;
|
||||||
|
expires_at: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [debugPopoverOpen, setDebugPopoverOpen] = useState(false);
|
const [debugPopoverOpen, setDebugPopoverOpen] = useState(false);
|
||||||
const [copiedDebugUrl, setCopiedDebugUrl] = useState(false);
|
const [copiedDebugUrl, setCopiedDebugUrl] = useState(false);
|
||||||
@@ -275,6 +276,13 @@ function PluginListView() {
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
{debugInfo?.expires_at && (
|
||||||
|
<p className="text-xs text-muted-foreground pl-[58px]">
|
||||||
|
{t('plugins.debugKeyExpires', {
|
||||||
|
time: new Date(debugInfo.expires_at).toLocaleString(),
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
{!debugInfo?.plugin_debug_key && (
|
{!debugInfo?.plugin_debug_key && (
|
||||||
<p className="text-xs text-muted-foreground ml-[58px]">
|
<p className="text-xs text-muted-foreground ml-[58px]">
|
||||||
{t('plugins.debugKeyDisabled')}
|
{t('plugins.debugKeyDisabled')}
|
||||||
|
|||||||
@@ -1091,6 +1091,7 @@ export class BackendClient extends BaseHttpClient {
|
|||||||
public getPluginDebugInfo(): Promise<{
|
public getPluginDebugInfo(): Promise<{
|
||||||
debug_url: string;
|
debug_url: string;
|
||||||
plugin_debug_key: string;
|
plugin_debug_key: string;
|
||||||
|
expires_at: string;
|
||||||
}> {
|
}> {
|
||||||
return this.get('/api/v1/plugins/debug-info');
|
return this.get('/api/v1/plugins/debug-info');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -521,9 +521,9 @@ const enUS = {
|
|||||||
debugInfoTitle: 'Plugin Debug Information',
|
debugInfoTitle: 'Plugin Debug Information',
|
||||||
debugUrl: 'Debug URL',
|
debugUrl: 'Debug URL',
|
||||||
debugKey: 'Debug Key',
|
debugKey: 'Debug Key',
|
||||||
|
debugKeyExpires: 'Rotates at {{time}}; each Workspace has a different key',
|
||||||
noDebugKey: '(Not Set)',
|
noDebugKey: '(Not Set)',
|
||||||
debugKeyDisabled:
|
debugKeyDisabled: 'Debug credential is temporarily unavailable',
|
||||||
'Debug key is not set, plugin debugging does not require authentication',
|
|
||||||
boxStatusTitle: 'Box Runtime',
|
boxStatusTitle: 'Box Runtime',
|
||||||
boxStatus: 'Status',
|
boxStatus: 'Status',
|
||||||
boxConnected: 'Connected',
|
boxConnected: 'Connected',
|
||||||
|
|||||||
@@ -535,9 +535,11 @@ const esES = {
|
|||||||
debugInfoTitle: 'Información de depuración del plugin',
|
debugInfoTitle: 'Información de depuración del plugin',
|
||||||
debugUrl: 'URL de depuración',
|
debugUrl: 'URL de depuración',
|
||||||
debugKey: 'Clave de depuración',
|
debugKey: 'Clave de depuración',
|
||||||
|
debugKeyExpires:
|
||||||
|
'Rota a las {{time}}; cada Workspace tiene una clave distinta',
|
||||||
noDebugKey: '(No establecida)',
|
noDebugKey: '(No establecida)',
|
||||||
debugKeyDisabled:
|
debugKeyDisabled:
|
||||||
'La clave de depuración no está configurada, la depuración del plugin no requiere autenticación',
|
'La credencial de depuración no está disponible temporalmente',
|
||||||
boxStatusTitle: 'Box Runtime',
|
boxStatusTitle: 'Box Runtime',
|
||||||
boxStatus: 'Estado',
|
boxStatus: 'Estado',
|
||||||
boxConnected: 'Conectado',
|
boxConnected: 'Conectado',
|
||||||
|
|||||||
@@ -527,9 +527,10 @@ const jaJP = {
|
|||||||
debugInfoTitle: 'プラグインデバッグ情報',
|
debugInfoTitle: 'プラグインデバッグ情報',
|
||||||
debugUrl: 'デバッグURL',
|
debugUrl: 'デバッグURL',
|
||||||
debugKey: 'デバッグキー',
|
debugKey: 'デバッグキー',
|
||||||
|
debugKeyExpires:
|
||||||
|
'{{time}} にローテーションします。Workspace ごとにキーが異なります',
|
||||||
noDebugKey: '(未設定)',
|
noDebugKey: '(未設定)',
|
||||||
debugKeyDisabled:
|
debugKeyDisabled: 'デバッグ認証情報を一時的に利用できません',
|
||||||
'デバッグキーが設定されていません。プラグインデバッグには認証が不要です',
|
|
||||||
boxStatusTitle: 'Box ランタイム',
|
boxStatusTitle: 'Box ランタイム',
|
||||||
boxStatus: 'ステータス',
|
boxStatus: 'ステータス',
|
||||||
boxConnected: '接続済み',
|
boxConnected: '接続済み',
|
||||||
|
|||||||
@@ -534,9 +534,9 @@ const ruRU = {
|
|||||||
debugInfoTitle: 'Отладочная информация плагина',
|
debugInfoTitle: 'Отладочная информация плагина',
|
||||||
debugUrl: 'URL для отладки',
|
debugUrl: 'URL для отладки',
|
||||||
debugKey: 'Ключ отладки',
|
debugKey: 'Ключ отладки',
|
||||||
|
debugKeyExpires: 'Смена в {{time}}; у каждого Workspace свой ключ',
|
||||||
noDebugKey: '(Не задан)',
|
noDebugKey: '(Не задан)',
|
||||||
debugKeyDisabled:
|
debugKeyDisabled: 'Учетные данные отладки временно недоступны',
|
||||||
'Ключ отладки не задан, аутентификация при отладке плагина не требуется',
|
|
||||||
boxStatusTitle: 'Box Runtime',
|
boxStatusTitle: 'Box Runtime',
|
||||||
boxStatus: 'Статус',
|
boxStatus: 'Статус',
|
||||||
boxConnected: 'Подключено',
|
boxConnected: 'Подключено',
|
||||||
|
|||||||
@@ -518,9 +518,9 @@ const thTH = {
|
|||||||
debugInfoTitle: 'ข้อมูลดีบักปลั๊กอิน',
|
debugInfoTitle: 'ข้อมูลดีบักปลั๊กอิน',
|
||||||
debugUrl: 'URL ดีบัก',
|
debugUrl: 'URL ดีบัก',
|
||||||
debugKey: 'คีย์ดีบัก',
|
debugKey: 'คีย์ดีบัก',
|
||||||
|
debugKeyExpires: 'หมุนเวียนเวลา {{time}}; แต่ละ Workspace ใช้คีย์ต่างกัน',
|
||||||
noDebugKey: '(ไม่ได้ตั้งค่า)',
|
noDebugKey: '(ไม่ได้ตั้งค่า)',
|
||||||
debugKeyDisabled:
|
debugKeyDisabled: 'ข้อมูลรับรองการดีบักไม่พร้อมใช้งานชั่วคราว',
|
||||||
'ไม่ได้ตั้งค่าคีย์ดีบัก การดีบักปลั๊กอินไม่ต้องยืนยันตัวตน',
|
|
||||||
boxStatusTitle: 'Box Runtime',
|
boxStatusTitle: 'Box Runtime',
|
||||||
boxStatus: 'สถานะ',
|
boxStatus: 'สถานะ',
|
||||||
boxConnected: 'เชื่อมต่อแล้ว',
|
boxConnected: 'เชื่อมต่อแล้ว',
|
||||||
|
|||||||
@@ -529,9 +529,9 @@ const viVN = {
|
|||||||
debugInfoTitle: 'Thông tin gỡ lỗi Plugin',
|
debugInfoTitle: 'Thông tin gỡ lỗi Plugin',
|
||||||
debugUrl: 'URL gỡ lỗi',
|
debugUrl: 'URL gỡ lỗi',
|
||||||
debugKey: 'Khóa gỡ lỗi',
|
debugKey: 'Khóa gỡ lỗi',
|
||||||
|
debugKeyExpires: 'Xoay vòng lúc {{time}}; mỗi Workspace có khóa riêng',
|
||||||
noDebugKey: '(Chưa đặt)',
|
noDebugKey: '(Chưa đặt)',
|
||||||
debugKeyDisabled:
|
debugKeyDisabled: 'Thông tin xác thực gỡ lỗi tạm thời không khả dụng',
|
||||||
'Khóa gỡ lỗi chưa được đặt, gỡ lỗi plugin không yêu cầu xác thực',
|
|
||||||
boxStatusTitle: 'Box Runtime',
|
boxStatusTitle: 'Box Runtime',
|
||||||
boxStatus: 'Trạng thái',
|
boxStatus: 'Trạng thái',
|
||||||
boxConnected: 'Đã kết nối',
|
boxConnected: 'Đã kết nối',
|
||||||
|
|||||||
@@ -496,8 +496,9 @@ const zhHans = {
|
|||||||
debugInfoTitle: '插件调试信息',
|
debugInfoTitle: '插件调试信息',
|
||||||
debugUrl: '调试地址',
|
debugUrl: '调试地址',
|
||||||
debugKey: '调试密钥',
|
debugKey: '调试密钥',
|
||||||
|
debugKeyExpires: '将于 {{time}} 轮换;每个工作区的密钥不同',
|
||||||
noDebugKey: '(未设置)',
|
noDebugKey: '(未设置)',
|
||||||
debugKeyDisabled: '未设置调试密钥,插件调试无需认证',
|
debugKeyDisabled: '调试凭据暂不可用',
|
||||||
boxStatusTitle: 'Box 运行时',
|
boxStatusTitle: 'Box 运行时',
|
||||||
boxStatus: '状态',
|
boxStatus: '状态',
|
||||||
boxConnected: '已连接',
|
boxConnected: '已连接',
|
||||||
|
|||||||
@@ -501,8 +501,9 @@ const zhHant = {
|
|||||||
debugInfoTitle: '外掛偵錯資訊',
|
debugInfoTitle: '外掛偵錯資訊',
|
||||||
debugUrl: '偵錯位址',
|
debugUrl: '偵錯位址',
|
||||||
debugKey: '偵錯金鑰',
|
debugKey: '偵錯金鑰',
|
||||||
|
debugKeyExpires: '將於 {{time}} 輪換;每個工作區的密鑰不同',
|
||||||
noDebugKey: '(未設定)',
|
noDebugKey: '(未設定)',
|
||||||
debugKeyDisabled: '未設定偵錯金鑰,外掛偵錯無需認證',
|
debugKeyDisabled: '偵錯憑據暫時無法使用',
|
||||||
boxStatusTitle: 'Box 執行時',
|
boxStatusTitle: 'Box 執行時',
|
||||||
boxStatus: '狀態',
|
boxStatus: '狀態',
|
||||||
boxConnected: '已連線',
|
boxConnected: '已連線',
|
||||||
|
|||||||
Reference in New Issue
Block a user