mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-08 23:13:17 +08:00
feat: localize IM settings
This commit is contained in:
parent
9d50ee7d03
commit
acd38ec3ee
16 changed files with 2161 additions and 1060 deletions
|
|
@ -60,6 +60,7 @@ QQ 扫码接入使用腾讯 QQBot v2 官方流程。默认腾讯授权页会把
|
|||
|
||||
- Harness 中只注册一个「IM机器人」设置页;
|
||||
- 九个渠道的 Host、客户端与运行时源码都在本仓库维护,不依赖外部独立渠道插件;
|
||||
- 设置页跟随 DeepSeek Harness 的语言选择,在中文和 English 之间即时切换;
|
||||
- 左侧使用渠道 Logo 切换微信、飞书、钉钉、企业微信、QQ、Slack、Telegram、Discord 和 WhatsApp,不使用启用/停用开关;
|
||||
- 九个渠道保持独立的 RPC、凭据、连接监督和会话映射;
|
||||
- 浏览器只获得二维码、Manifest 和脱敏状态;手动输入的 Secret 或 Token 仅单向提交给本机 Host,任何 RPC 响应都不会返回 App Secret、`bot_token`、钉钉 `client_secret`、企业微信 Secret、QQ `app_secret`、Slack Bot/App Token、Telegram/Discord Bot Token、WhatsApp 关联设备密钥或原始用户标识。
|
||||
|
|
@ -146,6 +147,7 @@ Feishu QR binding records the scanner as an allowed user. Manual credentials can
|
|||
|
||||
- Registers a single **IM Bot** settings page in Harness.
|
||||
- Maintains all nine channel Host, client, and runtime sources in this repository without external standalone channel plugins.
|
||||
- Follows the DeepSeek Harness language preference and switches the settings UI live between Chinese and English.
|
||||
- Uses channel logos for WeChat, Feishu, DingTalk, WeCom, QQ, Slack, Telegram, Discord, and WhatsApp navigation without enable/disable switches.
|
||||
- Keeps RPC endpoints, credentials, connection supervision, and session mappings isolated by channel.
|
||||
- Returns only QR codes, the public Slack Manifest, and redacted status data to the browser. Manually entered secrets and Tokens travel one way to the local Host; no RPC response returns App Secrets, `bot_token`, DingTalk `client_secret`, WeCom Secrets, QQ `app_secret`, Slack Bot/App Tokens, Telegram/Discord Bot Tokens, WhatsApp linked-device keys, or raw user identifiers.
|
||||
|
|
|
|||
2519
lib/client.js
2519
lib/client.js
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,7 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { CredentialActionIcon, CredentialBindingPanel, QrActionIcon } from '../../credential-binding.js';
|
||||
import { h } from '../../i18n.js';
|
||||
import {
|
||||
DINGTALK_ENDPOINTS,
|
||||
DINGTALK_RPC_CHANNEL,
|
||||
|
|
@ -13,7 +14,6 @@ import {
|
|||
} from './api.js';
|
||||
import { installDingtalkStyles } from './styles.js';
|
||||
|
||||
const h = React.createElement;
|
||||
const ACTIVE_PROVISION_STATES = new Set(['pending', 'scanned', 'authorizing', 'creating', 'connecting']);
|
||||
|
||||
export const name = 'dingtalk-settings';
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import * as React from "react";
|
|||
|
||||
import { FeishuLogoGlyph } from "../../channel-logos.js";
|
||||
import { CredentialActionIcon, CredentialBindingPanel, QrActionIcon } from "../../credential-binding.js";
|
||||
import { h } from "../../i18n.js";
|
||||
import {
|
||||
FEISHU_ENDPOINTS,
|
||||
FEISHU_RPC_CHANNEL,
|
||||
|
|
@ -15,8 +16,6 @@ import {
|
|||
import { useAnimationFrameScheduler } from "../../lifecycle.js";
|
||||
import { installFeishuStyles } from "./styles.js";
|
||||
|
||||
const h = React.createElement;
|
||||
|
||||
export const name = "feishu-settings";
|
||||
export const inject = ["slots", "connection"];
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import * as React from 'react';
|
|||
|
||||
import { QqLogoGlyph } from '../../channel-logos.js';
|
||||
import { CredentialActionIcon, CredentialBindingPanel, QrActionIcon } from '../../credential-binding.js';
|
||||
import { h } from '../../i18n.js';
|
||||
import { installDingtalkStyles } from '../dingtalk/styles.js';
|
||||
import {
|
||||
QQ_ENDPOINTS,
|
||||
|
|
@ -15,7 +16,6 @@ import {
|
|||
} from './api.js';
|
||||
import { installQqStyles } from './styles.js';
|
||||
|
||||
const h = React.createElement;
|
||||
const ACTIVE_STATES = new Set(['pending', 'refreshing', 'connecting']);
|
||||
|
||||
const Button = React.forwardRef(function Button({ children, kind = 'secondary', className = '', ...props }, ref) {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { CredentialActionIcon, CredentialBindingPanel } from '../../credential-binding.js';
|
||||
import { h } from '../../i18n.js';
|
||||
import { installDingtalkStyles } from '../dingtalk/styles.js';
|
||||
|
||||
const h = React.createElement;
|
||||
|
||||
const Button = React.forwardRef(function Button(
|
||||
{ children, kind = 'secondary', className = '', ...props },
|
||||
ref,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { SlackLogoGlyph } from '../../channel-logos.js';
|
||||
import { h } from '../../i18n.js';
|
||||
import { createTokenChannelSettings } from '../shared/token-channel.js';
|
||||
import {
|
||||
SLACK_CREATE_APP_URL,
|
||||
|
|
@ -9,8 +10,6 @@ import {
|
|||
import { SLACK_ENDPOINTS, slackClientApi } from './api.js';
|
||||
import { installSlackStyles } from './styles.js';
|
||||
|
||||
const h = React.createElement;
|
||||
|
||||
export function SlackCredentialPanel({ busy, error, onSubmit, onCancel }) {
|
||||
const [botToken, setBotToken] = React.useState('');
|
||||
const [appToken, setAppToken] = React.useState('');
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import * as React from 'react';
|
|||
|
||||
import { WecomLogoGlyph } from '../../channel-logos.js';
|
||||
import { CredentialActionIcon, CredentialBindingPanel, QrActionIcon } from '../../credential-binding.js';
|
||||
import { h } from '../../i18n.js';
|
||||
import { installDingtalkStyles } from '../dingtalk/styles.js';
|
||||
import {
|
||||
WECOM_ENDPOINTS,
|
||||
|
|
@ -15,7 +16,6 @@ import {
|
|||
} from './api.js';
|
||||
import { installWecomStyles } from './styles.js';
|
||||
|
||||
const h = React.createElement;
|
||||
const ACTIVE_STATES = new Set(['pending', 'refreshing', 'connecting']);
|
||||
|
||||
const Button = React.forwardRef(function Button({ children, kind = 'secondary', className = '', ...props }, ref) {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import * as React from 'react';
|
|||
|
||||
import { WeixinLogoGlyph } from '../../channel-logos.js';
|
||||
import { QrActionIcon } from '../../credential-binding.js';
|
||||
import { h } from '../../i18n.js';
|
||||
import {
|
||||
WEIXIN_ENDPOINTS,
|
||||
WEIXIN_RPC_CHANNEL,
|
||||
|
|
@ -16,8 +17,6 @@ import {
|
|||
import { createPollScheduler, useAnimationFrameScheduler } from '../../lifecycle.js';
|
||||
import { installWeixinStyles } from './styles.js';
|
||||
|
||||
const h = React.createElement;
|
||||
|
||||
export const name = 'weixin-settings';
|
||||
export const inject = ['slots', 'connection'];
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import * as React from 'react';
|
|||
|
||||
import { WhatsappLogoGlyph } from '../../channel-logos.js';
|
||||
import { QrActionIcon } from '../../credential-binding.js';
|
||||
import { h } from '../../i18n.js';
|
||||
import { installDingtalkStyles } from '../dingtalk/styles.js';
|
||||
import {
|
||||
WHATSAPP_ENDPOINTS,
|
||||
|
|
@ -14,7 +15,6 @@ import {
|
|||
} from './api.js';
|
||||
import { installWhatsappStyles } from './styles.js';
|
||||
|
||||
const h = React.createElement;
|
||||
const ACTIVE_STATES = new Set(['pending', 'connecting']);
|
||||
|
||||
const Button = React.forwardRef(function Button(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import * as React from 'react';
|
||||
|
||||
const h = React.createElement;
|
||||
import { h } from './i18n.js';
|
||||
|
||||
function ActionIcon({ children }) {
|
||||
return h('svg', {
|
||||
|
|
|
|||
443
plugin-src/client/i18n.js
Normal file
443
plugin-src/client/i18n.js
Normal file
|
|
@ -0,0 +1,443 @@
|
|||
import * as React from 'react';
|
||||
|
||||
export const IM_LOCALE_NAMESPACE = 'dsh-im';
|
||||
|
||||
const EN = Object.freeze({
|
||||
'$locale': 'en',
|
||||
'IM机器人': 'IM bots',
|
||||
'IM机器人设置': 'IM bot settings',
|
||||
'IM 渠道': 'IM channels',
|
||||
'把机器人接入 DeepSeek Harness': 'Connect bots to DeepSeek Harness',
|
||||
'微信': 'WeChat',
|
||||
'飞书': 'Feishu',
|
||||
'钉钉': 'DingTalk',
|
||||
'企业微信': 'WeCom',
|
||||
'微信机器人': 'WeChat bot',
|
||||
'飞书机器人': 'Feishu bot',
|
||||
'钉钉机器人': 'DingTalk bot',
|
||||
'企业微信机器人': 'WeCom bot',
|
||||
'QQ机器人': 'QQ bot',
|
||||
'WhatsApp机器人': 'WhatsApp bot',
|
||||
'WhatsApp账号': 'WhatsApp account',
|
||||
'微信设置': 'WeChat settings',
|
||||
'飞书机器人设置': 'Feishu bot settings',
|
||||
'钉钉设置': 'DingTalk settings',
|
||||
'企业微信设置': 'WeCom settings',
|
||||
'扫码接入机器人': 'Scan QR code',
|
||||
'正在接入': 'Connecting',
|
||||
'手动接入': 'Manual setup',
|
||||
'收起凭据': 'Hide credentials',
|
||||
'收起接入': 'Hide setup',
|
||||
'接入机器人': 'Connect bot',
|
||||
'开始接入': 'Start setup',
|
||||
'在线': 'online',
|
||||
'运行正常': 'Connected',
|
||||
'正在连接': 'Connecting',
|
||||
'正在连接…': 'Connecting…',
|
||||
'连接未就绪': 'Not connected',
|
||||
'连接中': 'Connecting',
|
||||
'连接中断': 'Disconnected',
|
||||
'需要处理': 'Needs attention',
|
||||
'状态未知': 'Unknown status',
|
||||
'离线': 'Offline',
|
||||
'已断开': 'Disconnected',
|
||||
'消息通道': 'Message channel',
|
||||
'最近检查': 'Last checked',
|
||||
'尚未检查': 'Not checked yet',
|
||||
'刚刚': 'Just now',
|
||||
'检查连接': 'Check connection',
|
||||
'检查中…': 'Checking…',
|
||||
'重试连接': 'Reconnect',
|
||||
'重试中…': 'Retrying…',
|
||||
'移除接入': 'Remove connection',
|
||||
'确认移除接入': 'Remove connection',
|
||||
'确认移除': 'Remove',
|
||||
'正在移除…': 'Removing…',
|
||||
'保留机器人': 'Keep bot',
|
||||
'保留账号': 'Keep account',
|
||||
'取消': 'Cancel',
|
||||
'关闭': 'Close',
|
||||
'立即重试': 'Retry now',
|
||||
'重新读取': 'Reload',
|
||||
'重新生成二维码': 'Generate a new QR code',
|
||||
'重新生成二维码后继续': 'Generate a new QR code',
|
||||
'刷新二维码': 'Refresh QR code',
|
||||
'刷新中…': 'Refreshing…',
|
||||
'换一个二维码': 'Get another QR code',
|
||||
'继续连接': 'Continue connecting',
|
||||
'绑定并连接': 'Connect',
|
||||
'正在绑定…': 'Connecting…',
|
||||
'验证并连接': 'Verify and connect',
|
||||
'正在验证并连接…': 'Verifying and connecting…',
|
||||
'正在验证…': 'Verifying…',
|
||||
'操作失败,请稍后重试': 'The operation failed. Try again later.',
|
||||
'请稍后重试': 'Try again later.',
|
||||
'当前二维码有效时间': 'QR code expires in',
|
||||
'二维码有效时间': 'QR code expires in',
|
||||
'二维码已过期': 'QR code expired',
|
||||
'二维码已失效': 'QR code expired',
|
||||
'二维码已过期\n请重新生成': 'QR code expired\nGenerate a new one',
|
||||
'二维码图片正在生成…': 'Generating QR code…',
|
||||
'二维码正在生成…': 'Generating QR code…',
|
||||
'二维码正在自动刷新…': 'Refreshing QR code…',
|
||||
'二维码未就绪,请打开授权链接': 'The QR code is not ready. Open the authorization link.',
|
||||
'二维码图片未就绪,请使用备用链接。': 'The QR code is not ready. Use the alternate link.',
|
||||
'二维码图片未就绪,请重新生成。': 'The QR code is not ready. Generate a new one.',
|
||||
'等待刷新': 'Waiting to refresh',
|
||||
'正在刷新二维码': 'Refreshing QR code',
|
||||
'打开备用链接': 'Open alternate link',
|
||||
'生成二维码': 'Generate QR code',
|
||||
'生成微信二维码': 'Generate WeChat QR code',
|
||||
'生成飞书二维码': 'Generate Feishu QR code',
|
||||
'生成钉钉二维码': 'Generate DingTalk QR code',
|
||||
'生成企业微信二维码': 'Generate WeCom QR code',
|
||||
'生成 QQ 二维码': 'Generate QQ QR code',
|
||||
'正在生成二维码…': 'Generating QR code…',
|
||||
'正在准备授权二维码': 'Preparing authorization QR code',
|
||||
'正在准备微信二维码': 'Preparing WeChat QR code',
|
||||
'正在添加新机器人': 'Adding a new bot',
|
||||
'正在申请钉钉授权二维码…': 'Requesting DingTalk authorization QR code…',
|
||||
'正在申请企业微信二维码…': 'Requesting WeCom QR code…',
|
||||
'正在申请 QQ 二维码…': 'Requesting QQ QR code…',
|
||||
'正在生成 WhatsApp 二维码': 'Generating WhatsApp QR code',
|
||||
'扫码,创建第一个飞书入口': 'Scan to create your first Feishu bot',
|
||||
'扫码只会新增一个机器人,已接入的机器人会继续正常收发消息。': 'Scanning adds one bot. Existing bots will continue to send and receive messages.',
|
||||
'无需手动填写 App ID。以后还可以继续添加机器人,分别服务不同团队或飞书租户。': 'No App ID is required. You can add more bots later for different teams or Feishu tenants.',
|
||||
'使用飞书扫码创建机器人': 'Scan with Feishu to create a bot',
|
||||
'刷新二维码后继续': 'Refresh the QR code to continue',
|
||||
'打开飞书移动端,使用扫一扫读取二维码': 'Open Feishu on your phone and scan the QR code',
|
||||
'核对应用名称与权限范围,并确认创建': 'Review the app name and permissions, then confirm',
|
||||
'保持本页打开,等待新机器人的长连接就绪': 'Keep this page open until the bot connection is ready',
|
||||
'在飞书中打开': 'Open in Feishu',
|
||||
'取消添加': 'Cancel',
|
||||
'已确认,正在连接新机器人': 'Confirmed. Connecting the new bot',
|
||||
'正在安全保存凭据并检查新机器人的消息通道,其他机器人不会中断。': 'Saving credentials and checking the new bot connection. Existing bots will not be interrupted.',
|
||||
'正在向飞书申请一次性授权二维码,请稍候。': 'Requesting a one-time authorization QR code from Feishu…',
|
||||
'新机器人没有添加完成': 'The new bot was not added',
|
||||
'新飞书机器人已连接,可以开始聊天。': 'The new Feishu bot is connected and ready to chat.',
|
||||
'飞书应用创建失败': 'Could not create the Feishu app',
|
||||
'机器人已经创建,但暂时无法确认连接状态': 'The bot was created, but its connection could not be confirmed yet',
|
||||
'机器人仍未连接': 'The bot is still offline',
|
||||
'机器人尚未连接': 'The bot is not connected yet',
|
||||
'长连接运行正常': 'Persistent connection is healthy',
|
||||
'长连接': 'Persistent connection',
|
||||
'应用标识已安全保存': 'App identifier stored securely',
|
||||
'机器人标识已安全保存': 'Bot identifier stored securely',
|
||||
'已安全保存': 'Stored securely',
|
||||
'已接入的微信账号': 'Connected WeChat accounts',
|
||||
'已接入的机器人': 'Connected bots',
|
||||
'已接入的钉钉机器人': 'Connected DingTalk bots',
|
||||
'已绑定的企业微信机器人': 'Connected WeCom bots',
|
||||
'已绑定的 QQ 机器人': 'Connected QQ bots',
|
||||
'已接入的 WhatsApp 机器人': 'Connected WhatsApp accounts',
|
||||
'使用手机微信扫描二维码': 'Scan with WeChat on your phone',
|
||||
'扫一次码,就能在微信里使用 Harness': 'Scan once to use Harness in WeChat',
|
||||
'打开手机微信并扫描左侧二维码': 'Open WeChat on your phone and scan the QR code',
|
||||
'在微信中确认连接该机器人': 'Confirm the bot connection in WeChat',
|
||||
'保持本页打开,等待机器人自动连接': 'Keep this page open while the bot connects',
|
||||
'等待微信扫码': 'Waiting for WeChat scan',
|
||||
'需要配对码': 'Pairing code required',
|
||||
'输入手机微信显示的数字': 'Enter the number shown in WeChat',
|
||||
'微信配对码': 'WeChat pairing code',
|
||||
'已扫码,请在手机上确认': 'Scanned. Confirm on your phone',
|
||||
'配对码已提交,正在等待微信确认。': 'Pairing code submitted. Waiting for WeChat confirmation.',
|
||||
'这是微信附加的安全确认步骤。配对码只用于本次扫码轮询,不会写入配置或日志。': 'This is an additional WeChat confirmation step. The pairing code is used only for this connection and is never stored.',
|
||||
'正在保存凭据并验证 Harness 与微信长轮询。': 'Saving credentials and verifying the WeChat connection.',
|
||||
'微信已确认,正在启动消息连接': 'Confirmed in WeChat. Starting the message connection',
|
||||
'微信已绑定,可以开始向已绑定的机器人发消息。': 'WeChat is connected and ready for messages.',
|
||||
'这个微信账号已经绑定并保持在线。': 'This WeChat account is connected and online.',
|
||||
'微信账号及本机凭据已移除。': 'The WeChat account and local credentials were removed.',
|
||||
'已取消微信绑定。': 'WeChat setup was cancelled.',
|
||||
'正在联系腾讯微信 iLink 服务。': 'Contacting the WeChat iLink service.',
|
||||
'iLink 长轮询': 'iLink long polling',
|
||||
'扫一次码,自动创建并连接机器人': 'Scan once to create and connect a bot',
|
||||
'使用钉钉 App 完成机器人授权': 'Authorize the bot with the DingTalk app',
|
||||
'使用已加入企业/组织的钉钉账号扫描左侧二维码': 'Scan the QR code with a DingTalk account that belongs to an organization',
|
||||
'在授权页点击“一键创建新机器人”': 'Select “Create new bot” on the authorization page',
|
||||
'请勿关闭本页,钉钉完成授权后将自动继续。': 'Keep this page open. Setup will continue after DingTalk authorization.',
|
||||
'等待钉钉扫码授权': 'Waiting for DingTalk authorization',
|
||||
'授权已确认,正在创建钉钉机器人': 'Authorized. Creating the DingTalk bot',
|
||||
'正在确认钉钉授权': 'Confirming DingTalk authorization',
|
||||
'正在检查钉钉 Stream 长连接,成功后会自动显示为在线。': 'Checking the DingTalk Stream connection. It will appear online when ready.',
|
||||
'钉钉机器人已接入,可以开始发送消息。': 'The DingTalk bot is connected and ready for messages.',
|
||||
'这个钉钉机器人已经接入并保持在线。': 'This DingTalk bot is connected and online.',
|
||||
'Stream 长连接': 'Stream persistent connection',
|
||||
'使用企业微信 App 扫码创建智能机器人': 'Scan with WeCom to create an AI bot',
|
||||
'使用企业微信 App 完成智能机器人授权': 'Authorize the AI bot with WeCom',
|
||||
'打开企业微信 App,扫描左侧二维码': 'Open WeCom and scan the QR code',
|
||||
'在腾讯授权页面确认创建智能机器人': 'Confirm bot creation on the Tencent authorization page',
|
||||
'返回这里等待连接完成': 'Return here and wait for the connection to complete',
|
||||
'等待企业微信 App 扫码': 'Waiting for WeCom scan',
|
||||
'企业微信已授权,正在连接机器人': 'Authorized in WeCom. Connecting the bot',
|
||||
'凭据正在写入本机,并启动企业微信 WebSocket 消息连接。': 'Saving credentials locally and starting the WeCom WebSocket connection.',
|
||||
'WebSocket 长连接': 'WebSocket persistent connection',
|
||||
'使用手机 QQ 扫码创建并绑定机器人': 'Scan with mobile QQ to create and connect a bot',
|
||||
'使用手机 QQ 完成机器人绑定': 'Complete bot setup with mobile QQ',
|
||||
'打开手机 QQ,扫描左侧二维码': 'Open mobile QQ and scan the QR code',
|
||||
'在腾讯授权页面确认创建或绑定机器人': 'Confirm bot creation or connection on the Tencent authorization page',
|
||||
'等待手机 QQ 扫码': 'Waiting for mobile QQ scan',
|
||||
'QQ 已授权,正在连接机器人': 'Authorized in QQ. Connecting the bot',
|
||||
'凭据正在写入本机,并启动 QQ WebSocket 消息连接。': 'Saving credentials locally and starting the QQ WebSocket connection.',
|
||||
'使用手机 WhatsApp 扫描二维码即可接入。': 'Scan the QR code with WhatsApp to connect.',
|
||||
'用手机 WhatsApp 扫描二维码': 'Scan with WhatsApp on your phone',
|
||||
'打开 WhatsApp → 设置 → 已关联设备': 'Open WhatsApp → Settings → Linked devices',
|
||||
'点击“关联设备”并扫描左侧二维码': 'Select “Link a device” and scan the QR code',
|
||||
'等待 WhatsApp 扫码': 'Waiting for WhatsApp scan',
|
||||
'已扫码,正在连接 WhatsApp': 'Scanned. Connecting WhatsApp',
|
||||
'正在建立安全的关联设备会话。': 'Creating a secure linked-device session.',
|
||||
'关联设备正在接入 DeepSeek Harness。': 'Linking the device to DeepSeek Harness.',
|
||||
'WhatsApp Web 关联设备运行正常': 'WhatsApp linked device is healthy',
|
||||
'Bot API 长轮询': 'Bot API long polling',
|
||||
' Gateway 长连接': ' Gateway persistent connection',
|
||||
'Gateway 长连接': 'Gateway persistent connection',
|
||||
' Socket Mode 长连接': ' Socket Mode persistent connection',
|
||||
'Socket Mode 长连接': 'Socket Mode persistent connection',
|
||||
'接入 Telegram 机器人': 'Connect a Telegram bot',
|
||||
'先通过 @BotFather 获取 Bot Token,再在这里完成接入。': 'Get a Bot Token from @BotFather, then connect it here.',
|
||||
'填写 @BotFather 生成的 Bot Token': 'Enter the Bot Token from @BotFather',
|
||||
'接入 Discord 机器人': 'Connect a Discord bot',
|
||||
'先在 Developer Portal 创建 Bot 并邀请到服务器,再在这里完成接入。': 'Create a bot in the Developer Portal and invite it to your server, then connect it here.',
|
||||
'填写 Discord Developer Portal 的 Bot Token': 'Enter the Bot Token from the Discord Developer Portal',
|
||||
'接入 Slack 机器人': 'Connect a Slack bot',
|
||||
'先用 Manifest 创建并配置 Slack App': 'Create and configure a Slack app with the manifest',
|
||||
'复制配置后,在 Slack 选择 From a manifest;创建完成后生成 connections:write App Token,并将应用安装到工作区。': 'Copy the manifest and choose “From a manifest” in Slack. Then create a connections:write App Token and install the app to your workspace.',
|
||||
'复制 Manifest': 'Copy manifest',
|
||||
'已复制 Manifest': 'Manifest copied',
|
||||
'打开 Slack 创建页': 'Open Slack app creation',
|
||||
'Bot Token 来自 OAuth & Permissions;App Token 来自 Basic Information,并且必须包含 connections:write。': 'Get the Bot Token from OAuth & Permissions and the App Token from Basic Information. The App Token must include connections:write.',
|
||||
'使用官方 App Manifest 快速配置机器人,再填写 Bot Token 与 App Token 建立本地 Socket Mode 连接。': 'Configure the bot with the official app manifest, then enter the Bot Token and App Token to start a local Socket Mode connection.',
|
||||
'Slack 工作区': 'Slack workspace',
|
||||
'Bot Token 与 App Token': 'Bot Token and App Token',
|
||||
'填写 Bot Token': 'Enter Bot Token',
|
||||
'手动接入飞书机器人': 'Connect Feishu bot manually',
|
||||
'手动接入钉钉机器人': 'Connect DingTalk bot manually',
|
||||
'手动接入企业微信机器人': 'Connect WeCom bot manually',
|
||||
'手动接入QQ机器人': 'Connect QQ bot manually',
|
||||
'填写飞书开放平台 App ID': 'Enter the Feishu Open Platform App ID',
|
||||
'填写飞书开放平台 App Secret': 'Enter the Feishu Open Platform App Secret',
|
||||
'填写钉钉应用 Client ID': 'Enter the DingTalk Client ID',
|
||||
'填写钉钉应用 Client Secret': 'Enter the DingTalk Client Secret',
|
||||
'填写企业微信智能机器人 Bot ID': 'Enter the WeCom AI Bot ID',
|
||||
'填写企业微信智能机器人 Secret': 'Enter the WeCom AI Bot Secret',
|
||||
'填写 QQ 开放平台 AppID': 'Enter the QQ Open Platform AppID',
|
||||
'填写 QQ 开放平台 AppSecret': 'Enter the QQ Open Platform AppSecret',
|
||||
'扫码接入微信机器人': 'Connect WeChat bot by QR code',
|
||||
'扫码接入飞书机器人': 'Connect Feishu bot by QR code',
|
||||
'扫码接入钉钉机器人': 'Connect DingTalk bot by QR code',
|
||||
'扫码接入企业微信机器人': 'Connect WeCom bot by QR code',
|
||||
'扫码接入 QQ 机器人': 'Connect QQ bot by QR code',
|
||||
'扫码接入 WhatsApp 机器人': 'Connect WhatsApp by QR code',
|
||||
'扫码绑定 WhatsApp 机器人': 'Connect WhatsApp by QR code',
|
||||
'使用 App ID 和 App Secret 绑定飞书机器人': 'Connect a Feishu bot with App ID and App Secret',
|
||||
'使用 Client ID 和 Client Secret 绑定钉钉机器人': 'Connect a DingTalk bot with Client ID and Client Secret',
|
||||
'使用 Bot ID 和 Secret 绑定企业微信机器人': 'Connect a WeCom bot with Bot ID and Secret',
|
||||
'使用 AppID 和 AppSecret 绑定 QQ 机器人': 'Connect a QQ bot with AppID and AppSecret',
|
||||
'使用 Manifest 和双 Token 接入 Slack 机器人': 'Connect a Slack bot with a manifest and two tokens',
|
||||
'使用 Bot Token 接入 Telegram 机器人': 'Connect a Telegram bot with a Bot Token',
|
||||
'使用 Bot Token 接入 Discord 机器人': 'Connect a Discord bot with a Bot Token',
|
||||
'取消绑定': 'Cancel setup',
|
||||
'取消接入': 'Cancel setup',
|
||||
'二维码由腾讯微信 iLink 服务签发。用手机微信扫描并确认后,账号凭据会直接写入 Harness Host,浏览器不会收到 bot_token。': 'The QR code is issued by Tencent WeChat iLink. After you scan and confirm, account credentials are written directly to the Harness Host and are never exposed to the browser.',
|
||||
'扫码账号必须已加入企业/组织。如果钉钉提示尚未加入组织,请在提示页创建组织,或换用已加入组织的账号。': 'The DingTalk account must belong to an organization. If prompted, create an organization or use an account that already belongs to one.',
|
||||
'请在手机上核对并确认授权。部分账号会额外显示一个配对数字,页面会在需要时提示输入。': 'Review and confirm authorization on your phone. Some accounts may also require a pairing number.',
|
||||
'授权由钉钉官方页面完成。扫码账号必须已加入一个企业/组织并有权创建机器人;创建成功后,应用凭据会直接写入 Harness Host。': 'Authorization is completed on DingTalk’s official page. The account must belong to an organization and be allowed to create bots. Credentials are written directly to the Harness Host.',
|
||||
'扫码由腾讯官方页面完成,不需要手动填写 AppID 或 AppSecret。扫码成功后,机器人会自动连接 DeepSeek Harness。': 'Scanning is completed on Tencent’s official page. No AppID or AppSecret is required, and the bot connects automatically.',
|
||||
'扫码由腾讯官方页面完成,不需要手动填写 Bot ID 或 Secret。创建成功后,机器人会自动连接 DeepSeek Harness。': 'Scanning is completed on Tencent’s official page. No Bot ID or Secret is required, and the bot connects automatically.',
|
||||
'腾讯页面会创建或绑定一个 QQ 机器人,并把连接凭据安全交给本机 Harness Host。': 'Tencent will create or connect a QQ bot and securely deliver its credentials to the local Harness Host.',
|
||||
'企业微信官方页面会创建一个智能机器人,并把连接凭据安全交给本机 Harness Host。': 'WeCom will create an AI bot and securely deliver its credentials to the local Harness Host.',
|
||||
'从此 Harness 移除这个微信账号?': 'Remove this WeChat account from Harness?',
|
||||
'这会停止消息连接,并删除本机保存的 bot_token、账号配置和会话映射。其他微信账号不受影响。': 'This stops the message connection and removes the locally stored bot_token, account configuration, and session mappings. Other WeChat accounts are not affected.',
|
||||
'此操作会停止这个机器人的连接,并删除保存在本机的接入配置和凭据。飞书开放平台中的应用不会被自动删除,其他机器人也不受影响。': 'This stops the bot connection and removes the locally stored configuration and credentials. The app in Feishu Open Platform is not deleted, and other bots are not affected.',
|
||||
'这会停止消息连接,并删除本机保存的应用凭据、机器人配置及会话映射。钉钉开放平台中的机器人不会被自动删除。': 'This stops the message connection and removes the locally stored app credentials, bot configuration, and session mappings. The bot in DingTalk Open Platform is not deleted.',
|
||||
'这会停止消息连接,并删除本机保存的应用凭据、机器人配置及会话映射。企业微信平台中的机器人不会被自动删除。': 'This stops the message connection and removes the locally stored app credentials, bot configuration, and session mappings. The bot in WeCom is not deleted.',
|
||||
'这会停止消息连接,并删除本机保存的应用凭据、机器人配置及会话映射。腾讯平台中的机器人不会被自动删除。': 'This stops the message connection and removes the locally stored app credentials, bot configuration, and session mappings. The bot on Tencent’s platform is not deleted.',
|
||||
'这会停止消息连接,并删除本机保存的 WhatsApp 关联设备和会话映射。': 'This stops the message connection and removes the locally stored WhatsApp linked device and session mappings.',
|
||||
'正在读取飞书机器人列表': 'Loading Feishu bots',
|
||||
'正在读取飞书连接状态…': 'Loading Feishu connection status…',
|
||||
'正在读取微信连接状态…': 'Loading WeChat connection status…',
|
||||
'正在读取钉钉连接状态…': 'Loading DingTalk connection status…',
|
||||
'通过扫码把钉钉机器人接入 DeepSeek Harness': 'Connect a DingTalk bot to DeepSeek Harness by QR code',
|
||||
'钉钉服务没有返回扫码绑定进度': 'DingTalk did not return QR setup progress',
|
||||
'钉钉扫码服务没有返回有效的绑定任务': 'DingTalk did not return a valid setup attempt',
|
||||
'钉钉 Stream 长连接运行正常': 'DingTalk Stream connection is healthy',
|
||||
'钉钉服务没有返回有效的机器人列表': 'DingTalk did not return a valid bot list',
|
||||
'${totals.connected} / ${totals.configured} 在线': '${totals.connected} / ${totals.configured} online',
|
||||
'用于把钉钉机器人接入 DeepSeek Harness 的一次性二维码': 'One-time QR code for connecting a DingTalk bot to DeepSeek Harness',
|
||||
'二维码已过期\\n请重新生成': 'QR code expired\\nGenerate a new one',
|
||||
'机器人已创建,正在建立消息连接': 'Bot created. Starting the message connection',
|
||||
'钉钉扫码服务没有返回安全的二维码': 'DingTalk did not return a secure QR code',
|
||||
'钉钉二维码已生成,请使用钉钉 App 扫描。': 'DingTalk QR code generated. Scan it with the DingTalk app.',
|
||||
'钉钉机器人凭据已绑定。': 'DingTalk bot credentials connected.',
|
||||
'已取消钉钉机器人接入。': 'DingTalk bot setup cancelled.',
|
||||
'钉钉机器人及本机凭据已移除。': 'DingTalk bot and local credentials removed.',
|
||||
'飞书服务没有返回二维码信息': 'Feishu did not return QR code information',
|
||||
'飞书服务返回的二维码信息不完整': 'Feishu returned incomplete QR code information',
|
||||
'飞书服务返回了无效的机器人状态': 'Feishu returned an invalid bot status',
|
||||
'飞书服务返回的机器人缺少 botId': 'The Feishu bot is missing botId',
|
||||
'飞书服务没有返回连接状态': 'Feishu did not return connection status',
|
||||
'飞书服务没有返回创建进度': 'Feishu did not return creation progress',
|
||||
'飞书服务返回了未知的创建状态': 'Feishu returned an unknown creation status',
|
||||
'已接入 ${totals.configured} 个机器人,其中 ${totals.connected} 个在线': '${totals.connected} of ${totals.configured} bots online',
|
||||
'尚未接入机器人': 'No bot connected yet',
|
||||
'用于新增 DeepSeek Harness 飞书机器人的一次性授权二维码': 'One-time authorization QR code for adding a Feishu bot to DeepSeek Harness',
|
||||
'请刷新后重新扫码': 'Refresh and scan again',
|
||||
'${connected ? "检查连接" : "重试连接"}${bot.name}': '${connected ? "Check connection" : "Reconnect"} ${bot.name}',
|
||||
'无法读取飞书机器人': 'Could not load Feishu bots',
|
||||
'授权二维码已生成,请使用飞书扫码。': 'Authorization QR code generated. Scan it with Feishu.',
|
||||
'飞书机器人凭据已绑定。': 'Feishu bot credentials connected.',
|
||||
'已取消添加机器人。': 'Adding the bot was cancelled.',
|
||||
'${newBot.bot.name}已连接,可以在飞书中开始聊天。': '${newBot.bot.name} is connected and ready to chat in Feishu.',
|
||||
'${bot.name}操作失败,请查看机器人状态。': '${bot.name} operation failed. Check the bot status.',
|
||||
'${bot.name}已从此 DeepSeek Harness 移除;飞书开放平台中的应用未被删除。': '${bot.name} was removed from this DeepSeek Harness. The app in Feishu Open Platform was not deleted.',
|
||||
'无法读取连接状态': 'Could not load connection status',
|
||||
'QQ 服务没有返回扫码绑定进度': 'QQ did not return QR setup progress',
|
||||
'QQ 扫码服务没有返回有效的绑定任务': 'QQ did not return a valid setup attempt',
|
||||
'QQ WebSocket 长连接运行正常': 'QQ WebSocket connection is healthy',
|
||||
'QQ 服务没有返回有效的机器人列表': 'QQ did not return a valid bot list',
|
||||
'尚未绑定 QQ 机器人': 'No QQ bot connected yet',
|
||||
'用于绑定 QQ 机器人的一次性二维码': 'One-time QR code for connecting a QQ bot',
|
||||
'${channel}${connectionSummary}运行正常': '${channel}${connectionSummary} is healthy',
|
||||
'${channel} 服务没有返回有效的机器人列表': '${channel} did not return a valid bot list',
|
||||
'使用 Bot Token 接入 ${channel} 机器人': 'Connect a ${channel} bot with a Bot Token',
|
||||
'${model.totals.connected} / ${model.totals.configured} 在线': '${model.totals.connected}/${model.totals.configured} online',
|
||||
' Bot API 长轮询': ' Bot API long polling',
|
||||
'企业微信服务没有返回扫码绑定进度': 'WeCom did not return QR setup progress',
|
||||
'企业微信扫码服务没有返回有效的绑定任务': 'WeCom did not return a valid setup attempt',
|
||||
'企业微信 WebSocket 长连接运行正常': 'WeCom WebSocket connection is healthy',
|
||||
'企业微信服务没有返回有效的机器人列表': 'WeCom did not return a valid bot list',
|
||||
'尚未绑定企业微信机器人': 'No WeCom bot connected yet',
|
||||
'用于绑定企业微信机器人的一次性二维码': 'One-time QR code for connecting a WeCom bot',
|
||||
'微信扫码服务没有返回有效的绑定任务': 'WeChat did not return a valid setup attempt',
|
||||
'微信绑定没有完成': 'WeChat setup did not complete',
|
||||
'微信连接正常': 'WeChat connection is healthy',
|
||||
'微信连接未就绪': 'WeChat connection is not ready',
|
||||
'微信服务没有返回有效的账号列表': 'WeChat did not return a valid account list',
|
||||
'尚未绑定微信': 'No WeChat account connected yet',
|
||||
'用于把微信机器人绑定到 DeepSeek Harness 的一次性二维码': 'One-time QR code for connecting a WeChat bot to DeepSeek Harness',
|
||||
'保持本页打开,等待消息长轮询变为在线': 'Keep this page open until long polling is online',
|
||||
'微信二维码已生成,请使用手机微信扫描。': 'WeChat QR code generated. Scan it with WeChat on your phone.',
|
||||
'移除失败:${presentError(error).message}': 'Removal failed: ${presentError(error).message}',
|
||||
'无法读取微信状态': 'Could not load WeChat status',
|
||||
'WhatsApp 服务没有返回扫码进度': 'WhatsApp did not return QR setup progress',
|
||||
'WhatsApp 服务没有返回有效的扫码任务': 'WhatsApp did not return a valid setup attempt',
|
||||
'WhatsApp 服务没有返回有效的机器人列表': 'WhatsApp did not return a valid account list',
|
||||
'用于关联 WhatsApp 设备的一次性二维码': 'One-time QR code for linking a WhatsApp device',
|
||||
});
|
||||
|
||||
export const en = EN;
|
||||
export const zh = Object.freeze(Object.fromEntries(
|
||||
Object.keys(EN).map((key) => [key, key === '$locale' ? 'zh' : key]),
|
||||
));
|
||||
|
||||
let translate = (key) => key;
|
||||
|
||||
export function setImTranslator(next) {
|
||||
translate = typeof next === 'function' ? next : (key) => key;
|
||||
}
|
||||
|
||||
export function isEnglish() {
|
||||
return translate('$locale') === 'en';
|
||||
}
|
||||
|
||||
function channelName(value) {
|
||||
return localizeText(value);
|
||||
}
|
||||
|
||||
function translateDynamic(text) {
|
||||
let match = /^(\d+) \/ (\d+) 在线$/.exec(text);
|
||||
if (match) return `${match[1]}/${match[2]} online`;
|
||||
match = /^已接入 (\d+) 个机器人,其中 (\d+) 个在线$/.exec(text);
|
||||
if (match) return `${match[2]} of ${match[1]} bots online`;
|
||||
match = /^正在读取\s*(.+?)\s*机器人状态…$/.exec(text);
|
||||
if (match) return `Loading ${channelName(match[1])} bot status…`;
|
||||
match = /^无法读取\s*(.+?)\s*机器人状态$/.exec(text);
|
||||
if (match) return `Could not load ${channelName(match[1])} bot status`;
|
||||
match = /^尚未接入\s*(.+?)\s*机器人$/.exec(text);
|
||||
if (match) return `No ${channelName(match[1])} bot connected yet`;
|
||||
match = /^已接入的\s*(.+?)\s*机器人$/.exec(text);
|
||||
if (match) return `Connected ${channelName(match[1])} bots`;
|
||||
match = /^手动接入(.+)机器人$/.exec(text);
|
||||
if (match) return `Connect ${channelName(match[1])} bot manually`;
|
||||
match = /^(.+) 设置$/.exec(text);
|
||||
if (match) return `${channelName(match[1])} settings`;
|
||||
match = /^从 DeepSeek Harness 移除“(.+)”?$/.exec(text);
|
||||
if (match) return `Remove “${match[1]}” from DeepSeek Harness?`;
|
||||
match = /^从 DeepSeek Harness 移除(.+)$/.exec(text);
|
||||
if (match) return `Remove ${match[1]} from DeepSeek Harness`;
|
||||
match = /^(检查连接|重试连接)(.+)$/.exec(text);
|
||||
if (match) return `${localizeText(match[1])} ${match[2]}`;
|
||||
match = /^移除(.+)$/.exec(text);
|
||||
if (match) return `Remove ${match[1]}`;
|
||||
match = /^这会停止消息连接,并删除本机保存的 (.+)、机器人配置及会话映射。(.+)中的机器人不会被自动删除。$/.exec(text);
|
||||
if (match) {
|
||||
return `This stops the message connection and removes the locally stored ${localizeText(match[1])}, bot configuration, and session mappings. The bot in ${localizeText(match[2])} is not deleted.`;
|
||||
}
|
||||
match = /^二维码剩余 (.+)$/.exec(text);
|
||||
if (match) return `QR code expires in ${match[1]}`;
|
||||
match = /^状态刷新失败:(.+)$/.exec(text);
|
||||
if (match) return `Status refresh failed: ${match[1]}`;
|
||||
match = /^状态自动刷新失败:(.+)$/.exec(text);
|
||||
if (match) return `Automatic status refresh failed: ${match[1]}`;
|
||||
match = /^操作失败:(.+)$/.exec(text);
|
||||
if (match) return `Operation failed: ${match[1]}`;
|
||||
match = /^连接检查失败:(.+)$/.exec(text);
|
||||
if (match) return `Connection check failed: ${match[1]}`;
|
||||
match = /^移除失败:(.+)$/.exec(text);
|
||||
if (match) return `Removal failed: ${match[1]}`;
|
||||
|
||||
const phrases = [
|
||||
['企业微信', 'WeCom'], ['DeepSeek Harness', 'DeepSeek Harness'],
|
||||
['WhatsApp', 'WhatsApp'], ['Telegram', 'Telegram'], ['Discord', 'Discord'],
|
||||
['Slack', 'Slack'], ['飞书', 'Feishu'], ['钉钉', 'DingTalk'], ['微信', 'WeChat'],
|
||||
['机器人', 'bot'], ['账号', 'account'], ['应用', 'app'], ['凭据', 'credentials'],
|
||||
['服务返回了无法识别的响应', 'service returned an unrecognized response'],
|
||||
['服务没有返回有效的机器人列表', 'service did not return a valid bot list'],
|
||||
['操作失败,请稍后重试', 'operation failed; try again later'],
|
||||
['操作失败', 'operation failed'], ['连接尚未就绪', 'connection is not ready'],
|
||||
['没有接入完成', 'was not connected'], ['没有绑定完成', 'was not connected'],
|
||||
['设置页缺少 RPC 连接', 'settings are missing an RPC connection'],
|
||||
['设置', 'settings'], ['连接检查完成', 'connection check completed'],
|
||||
['仍未连接,插件会继续自动重试', 'is still offline; the plugin will keep retrying'],
|
||||
['已重新连接', 'reconnected'], ['移除失败,请重试', 'could not be removed; try again'],
|
||||
['已连接,可以开始聊天', 'is connected and ready to chat'],
|
||||
['已连接,可以开始发送消息', 'is connected and ready for messages'],
|
||||
['服务请求失败', 'service request failed'], ['连接遇到问题', 'connection encountered a problem'],
|
||||
['正在读取', 'Loading '], ['连接状态', 'connection status'], ['二维码', 'QR code'],
|
||||
];
|
||||
let output = text;
|
||||
for (const [source, target] of phrases) output = output.replaceAll(source, target);
|
||||
return output;
|
||||
}
|
||||
|
||||
export function localizeText(value) {
|
||||
if (typeof value !== 'string') return value;
|
||||
const exact = translate(value);
|
||||
if (exact !== value || !isEnglish()) return exact;
|
||||
return translateDynamic(value);
|
||||
}
|
||||
|
||||
const LOCALIZED_PROPS = Object.freeze([
|
||||
'aria-label',
|
||||
'alt',
|
||||
'placeholder',
|
||||
'title',
|
||||
]);
|
||||
|
||||
function localizeChild(child) {
|
||||
if (typeof child === 'string') return localizeText(child);
|
||||
if (Array.isArray(child)) return child.map(localizeChild);
|
||||
return child;
|
||||
}
|
||||
|
||||
export function h(type, props, ...children) {
|
||||
let localizedProps = props;
|
||||
if (props) {
|
||||
for (const key of LOCALIZED_PROPS) {
|
||||
if (typeof props[key] === 'string') {
|
||||
localizedProps = localizedProps === props ? { ...props } : localizedProps;
|
||||
localizedProps[key] = localizeText(props[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return React.createElement(type, localizedProps, ...children.map(localizeChild));
|
||||
}
|
||||
|
|
@ -37,12 +37,11 @@ import { installWeixinStyles } from './channels/weixin/styles.js';
|
|||
import { WHATSAPP_RPC_CHANNEL } from './channels/whatsapp/api.js';
|
||||
import { WhatsappSettingsTab } from './channels/whatsapp/index.js';
|
||||
import { installWhatsappStyles } from './channels/whatsapp/styles.js';
|
||||
import { en, h, IM_LOCALE_NAMESPACE, setImTranslator, zh } from './i18n.js';
|
||||
import { installImStyles } from './styles.js';
|
||||
|
||||
const h = React.createElement;
|
||||
|
||||
export const name = 'im-settings';
|
||||
export const inject = ['slots', 'connection'];
|
||||
export const inject = ['slots', 'connection', 'locale'];
|
||||
|
||||
const CHANNELS = Object.freeze([
|
||||
{ id: 'weixin', label: '微信' },
|
||||
|
|
@ -172,6 +171,13 @@ export function IMSettingsTab({
|
|||
}
|
||||
|
||||
export function apply(ctx) {
|
||||
ctx.effect(
|
||||
() => ctx.locale.register(IM_LOCALE_NAMESPACE, { zh, en }),
|
||||
'im-settings: bilingual dictionaries',
|
||||
);
|
||||
const t = ctx.locale.bind(IM_LOCALE_NAMESPACE);
|
||||
setImTranslator(t);
|
||||
|
||||
ctx.effect(() => {
|
||||
const disposers = [
|
||||
installFeishuStyles(),
|
||||
|
|
@ -212,7 +218,8 @@ export function apply(ctx) {
|
|||
name: 'settings.plugins.tab',
|
||||
id: 'im',
|
||||
order: 20,
|
||||
label: 'IM机器人',
|
||||
label: () => t('IM机器人'),
|
||||
locale: IM_LOCALE_NAMESPACE,
|
||||
inject: () => ({
|
||||
dingtalkRpcCall,
|
||||
discordRpcCall,
|
||||
|
|
|
|||
|
|
@ -47,15 +47,16 @@ const CSS = String.raw`
|
|||
.dim-panel { min-width: 0; container-type: inline-size; }
|
||||
.dim-panel .bxf-page, .dim-panel .dxw-page, .dim-panel .ddt-page, .dim-panel .dqq-page, .dim-panel .dwecom-page, .dim-panel .dsl-page, .dim-panel .dwa-page { width: 100%; max-width: none; padding: 0 0 24px; }
|
||||
.dim-panel .bxf-heading, .dim-panel .dxw-heading, .dim-panel .ddt-heading { justify-content: flex-end; }
|
||||
.dim-panel .bxf-headingTools, .dim-panel .dxw-tools, .dim-panel .ddt-tools { width: 100%; justify-content: space-between; }
|
||||
.dim-panel .bxf-headingTools, .dim-panel .dxw-tools, .dim-panel .ddt-tools { width: 100%; display: grid; grid-template-columns: minmax(0, 1fr) max-content; align-items: center; justify-content: stretch; gap: 8px; }
|
||||
.dim-panel .dim-bindActions { min-width: 0; display: flex; align-items: center; flex-wrap: nowrap; gap: 8px; }
|
||||
.dim-panel .bxf-headingTools .dim-scanButton, .dim-panel .dxw-tools .dim-scanButton, .dim-panel .ddt-tools .dim-scanButton { flex: none; min-height: 34px; display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 0 10px; border: 1px solid #1677ff; border-radius: 8px; color: #fff; background: #1677ff; box-shadow: none; font: inherit; font-size: 13px; font-weight: 560; white-space: nowrap; }
|
||||
.dim-panel .dim-bindActions > button { min-width: 0; }
|
||||
.dim-panel .bxf-headingTools .dim-scanButton, .dim-panel .dxw-tools .dim-scanButton, .dim-panel .ddt-tools .dim-scanButton { flex: none; min-height: 34px; display: inline-flex; align-items: center; justify-content: center; justify-self: start; gap: 6px; padding: 0 10px; border: 1px solid #1677ff; border-radius: 8px; color: #fff; background: #1677ff; box-shadow: none; font: inherit; font-size: 13px; font-weight: 560; white-space: nowrap; }
|
||||
.dim-panel .bxf-headingTools .dim-scanButton:hover:not(:disabled), .dim-panel .dxw-tools .dim-scanButton:hover:not(:disabled), .dim-panel .ddt-tools .dim-scanButton:hover:not(:disabled) { border-color: #0958d9; background: #0958d9; }
|
||||
.dim-panel .dim-credentialButton { flex: none; min-height: 34px; display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 0 10px; border: 1px solid #86909c; border-radius: 8px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-1, #fff); box-shadow: 0 1px 2px rgb(31 35 41 / 5%); font: inherit; font-size: 13px; font-weight: 560; line-height: normal; white-space: nowrap; }
|
||||
.dim-panel .dim-actionIcon { width: 15px; height: 15px; flex: 0 0 15px; }
|
||||
.dim-panel .dim-credentialButton:hover:not(:disabled) { border-color: #4e5969; background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
|
||||
.dim-panel .dim-credentialButton[aria-pressed="true"] { border-color: #4e5969; background: var(--dsw-alias-bg-module-platform, #f2f3f5); box-shadow: inset 0 0 0 1px rgb(78 89 105 / 8%); }
|
||||
.dim-panel .bxf-headingTools .dim-onlineBadge, .dim-panel .dxw-tools .dim-onlineBadge, .dim-panel .ddt-tools .dim-onlineBadge { flex: none; min-height: 30px; display: inline-flex; align-items: center; gap: 0; padding: 0 11px; border: 0; border-radius: 999px; color: var(--dsw-alias-label-secondary, #646a73); background: var(--dsw-alias-bg-module-platform, #f2f3f5); font: inherit; font-size: 12px; font-weight: 400; line-height: normal; white-space: nowrap; }
|
||||
.dim-panel .bxf-headingTools .dim-onlineBadge, .dim-panel .dxw-tools .dim-onlineBadge, .dim-panel .ddt-tools .dim-onlineBadge { min-height: 30px; display: inline-flex; align-items: center; justify-self: end; gap: 0; padding: 0 11px; border: 0; border-radius: 999px; color: var(--dsw-alias-label-secondary, #646a73); background: var(--dsw-alias-bg-module-platform, #f2f3f5); font: inherit; font-size: 12px; font-weight: 400; line-height: normal; white-space: nowrap; }
|
||||
.dim-panel .dim-channelPage { width: 100%; max-width: none; display: flex; flex-direction: column; gap: 18px; padding: 0 0 24px; color: var(--dsw-alias-label-primary, #1f2329); box-sizing: border-box; }
|
||||
.dim-panel .dim-surfaceCard { position: relative; overflow: hidden; border: 1px solid var(--dsw-alias-border-l2, #e5e6eb); border-radius: 14px; background: var(--dsw-alias-bg-layer-1, #fff); box-shadow: 0 1px 2px rgb(31 35 41 / 3%); }
|
||||
.dim-panel .dim-surfaceCard::before { display: none; }
|
||||
|
|
@ -151,6 +152,11 @@ const CSS = String.raw`
|
|||
.dim-panel .ddt-headingCopy { display: none; }
|
||||
.dim-panel .ddt-qrFrame, .dim-panel .ddt-countdown { width: min(270px, 100%); }
|
||||
@container (max-width: 680px) {
|
||||
.dim-panel .bxf-headingTools, .dim-panel .dxw-tools, .dim-panel .ddt-tools { gap: 6px; }
|
||||
.dim-panel .dim-bindActions { gap: 6px; }
|
||||
.dim-panel .bxf-headingTools .dim-scanButton, .dim-panel .dxw-tools .dim-scanButton, .dim-panel .ddt-tools .dim-scanButton, .dim-panel .dim-credentialButton { gap: 5px; padding-inline: 8px; font-size: 12px; }
|
||||
.dim-panel .dim-actionIcon { width: 13px; height: 13px; flex-basis: 13px; }
|
||||
.dim-panel .bxf-headingTools .dim-onlineBadge, .dim-panel .dxw-tools .dim-onlineBadge, .dim-panel .ddt-tools .dim-onlineBadge { padding-inline: 8px; font-size: 11px; }
|
||||
.dim-panel .dim-credentialForm { grid-template-columns: minmax(0, 1fr); }
|
||||
.dim-panel .dim-credentialError, .dim-panel .dim-credentialActions { grid-column: auto; }
|
||||
.dim-panel .dim-emptyView { min-height: 0; grid-template-columns: minmax(0, 1fr); }
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ const required = [
|
|||
'THIRD_PARTY_NOTICES.md',
|
||||
'plugin-src/client/channels/dingtalk/index.js',
|
||||
'plugin-src/client/channels/slack/index.js',
|
||||
'plugin-src/client/i18n.js',
|
||||
'plugin-src/host/channels/feishu/index.mjs',
|
||||
'plugin-src/host/channels/weixin/index.mjs',
|
||||
'plugin-src/host/channels/dingtalk/index.mjs',
|
||||
|
|
@ -50,8 +51,11 @@ const lock = JSON.parse(lockText);
|
|||
if (!client.includes('id: "@xmanrui/dsh-im"')) {
|
||||
throw new Error('client bundle does not register the dsh-im loader id');
|
||||
}
|
||||
if (!client.includes('id: "im"') || !client.includes('label: "IM\\u673A\\u5668\\u4EBA"')) {
|
||||
throw new Error('client bundle does not register the IM机器人 settings tab');
|
||||
if (!client.includes('id: "im"')
|
||||
|| !client.includes('label: () => t("IM\\u673A\\u5668\\u4EBA")')
|
||||
|| !client.includes('locale: IM_LOCALE_NAMESPACE')
|
||||
|| !client.includes('IM_LOCALE_NAMESPACE = "dsh-im"')) {
|
||||
throw new Error('client bundle does not register the localized IM settings tab');
|
||||
}
|
||||
if ((client.match(/ctx\.slots\.inject\("settings\.plugins\.tab"/g) ?? []).length !== 1) {
|
||||
throw new Error('client bundle must register exactly one settings tab');
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { readFile, readdir } from 'node:fs/promises';
|
||||
import test from 'node:test';
|
||||
|
||||
import React from 'react';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
|
||||
import { IMSettingsTab } from '../plugin-src/client/index.js';
|
||||
import {
|
||||
apply as applyClient,
|
||||
IMSettingsTab,
|
||||
inject as clientInject,
|
||||
} from '../plugin-src/client/index.js';
|
||||
import { CredentialBindingPanel } from '../plugin-src/client/credential-binding.js';
|
||||
import { DINGTALK_ENDPOINTS } from '../plugin-src/client/channels/dingtalk/api.js';
|
||||
import {
|
||||
|
|
@ -24,7 +28,33 @@ import {
|
|||
AccountCard as WecomAccountCard,
|
||||
WecomSettingsTab,
|
||||
} from '../plugin-src/client/channels/wecom/index.js';
|
||||
import { QqSettingsTab } from '../plugin-src/client/channels/qq/index.js';
|
||||
import {
|
||||
AccountCard as QqAccountCard,
|
||||
QqSettingsTab,
|
||||
} from '../plugin-src/client/channels/qq/index.js';
|
||||
import {
|
||||
SlackAccountCard,
|
||||
SlackSettingsTab,
|
||||
} from '../plugin-src/client/channels/slack/index.js';
|
||||
import {
|
||||
TelegramAccountCard,
|
||||
TelegramSettingsTab,
|
||||
} from '../plugin-src/client/channels/telegram/index.js';
|
||||
import {
|
||||
DiscordAccountCard,
|
||||
DiscordSettingsTab,
|
||||
} from '../plugin-src/client/channels/discord/index.js';
|
||||
import {
|
||||
WhatsappAccountCard,
|
||||
WhatsappSettingsTab,
|
||||
} from '../plugin-src/client/channels/whatsapp/index.js';
|
||||
import {
|
||||
en,
|
||||
IM_LOCALE_NAMESPACE,
|
||||
localizeText,
|
||||
setImTranslator,
|
||||
zh,
|
||||
} from '../plugin-src/client/i18n.js';
|
||||
|
||||
const STYLES_URL = new URL('../plugin-src/client/styles.js', import.meta.url);
|
||||
const FEISHU_STYLES_URL = new URL(
|
||||
|
|
@ -52,6 +82,7 @@ const WEIXIN_SOURCE_URL = new URL(
|
|||
import.meta.url,
|
||||
);
|
||||
const CLIENT_BUNDLE_URL = new URL('../lib/client.js', import.meta.url);
|
||||
const CLIENT_SOURCE_DIRECTORY_URL = new URL('../plugin-src/client/', import.meta.url);
|
||||
const DINGTALK_CLIENT_SOURCE_URL = new URL(
|
||||
'../plugin-src/client/channels/dingtalk/index.js',
|
||||
import.meta.url,
|
||||
|
|
@ -263,7 +294,10 @@ test('scan actions align left while online totals align right in every channel',
|
|||
readFile(WECOM_SOURCE_URL, 'utf8'),
|
||||
]);
|
||||
|
||||
assert.match(imStyles, /\.dim-panel \.bxf-headingTools, \.dim-panel \.dxw-tools, \.dim-panel \.ddt-tools \{[^}]*justify-content: space-between;/);
|
||||
assert.match(imStyles, /\.dim-panel \.bxf-headingTools, \.dim-panel \.dxw-tools, \.dim-panel \.ddt-tools \{[^}]*display: grid;[^}]*grid-template-columns: minmax\(0, 1fr\) max-content;[^}]*justify-content: stretch;/);
|
||||
assert.match(imStyles, /\.dim-panel \.dim-bindActions > button \{[^}]*min-width: 0;/);
|
||||
assert.match(imStyles, /\.dim-panel \.bxf-headingTools \.dim-scanButton,[^}]*justify-self: start;/);
|
||||
assert.match(imStyles, /\.dim-panel \.bxf-headingTools \.dim-onlineBadge,[^}]*justify-self: end;/);
|
||||
assert.match(feishuStyles, /\.bxf-headingTools \{[^}]*justify-content: space-between;/);
|
||||
assert.match(weixinStyles, /\.dxw-tools \{[^}]*justify-content: space-between;/);
|
||||
assert.match(dingtalkStyles, /\.ddt-tools \{[^}]*justify-content: space-between;/);
|
||||
|
|
@ -482,3 +516,160 @@ test('the bundled DingTalk channel has no local sender approval workflow', async
|
|||
/bot\.sender\.approve|bot\.sender\.revoke|允许使用机器人的钉钉账号|批准使用/,
|
||||
);
|
||||
});
|
||||
|
||||
test('every shipped Chinese client string has an English projection', async () => {
|
||||
const paths = (await readdir(CLIENT_SOURCE_DIRECTORY_URL, { recursive: true }))
|
||||
.filter((path) => path.endsWith('.js') && path !== 'i18n.js');
|
||||
const sources = await Promise.all(paths.map((path) =>
|
||||
readFile(new URL(path, CLIENT_SOURCE_DIRECTORY_URL), 'utf8')));
|
||||
const strings = new Set();
|
||||
for (const source of sources) {
|
||||
for (const match of source.matchAll(/(['"`])((?:\\.|(?!\1)[\s\S])*?)\1/g)) {
|
||||
if (/[\p{Script=Han}]/u.test(match[2])) strings.add(match[2]);
|
||||
}
|
||||
}
|
||||
|
||||
setImTranslator((key) => en[key] ?? key);
|
||||
try {
|
||||
const untranslated = [...strings].filter((value) =>
|
||||
/[\p{Script=Han}]/u.test(localizeText(value)));
|
||||
assert.deepEqual(untranslated, []);
|
||||
assert.ok(strings.size > 350);
|
||||
} finally {
|
||||
setImTranslator(null);
|
||||
}
|
||||
});
|
||||
|
||||
test('client registers a live bilingual locale seat for the IM settings tab', () => {
|
||||
const effects = [];
|
||||
const registrations = [];
|
||||
const dictionaries = [];
|
||||
const rpcCall = async () => ({ ok: true, value: {} });
|
||||
const ctx = {
|
||||
effect(install, label) {
|
||||
effects.push({ install, label });
|
||||
},
|
||||
locale: {
|
||||
bind(namespace) {
|
||||
assert.equal(namespace, IM_LOCALE_NAMESPACE);
|
||||
return (key) => en[key] ?? key;
|
||||
},
|
||||
register(namespace, value) {
|
||||
dictionaries.push({ namespace, value });
|
||||
return () => {};
|
||||
},
|
||||
},
|
||||
connection: { rpc: { call: rpcCall } },
|
||||
slots: {
|
||||
inject(name, install) {
|
||||
assert.equal(name, 'settings.plugins.tab');
|
||||
install();
|
||||
},
|
||||
register(options, component) {
|
||||
registrations.push({ options, component });
|
||||
return () => {};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
applyClient(ctx);
|
||||
const dictionaryEffect = effects.find((entry) => entry.label === 'im-settings: bilingual dictionaries');
|
||||
assert.ok(dictionaryEffect);
|
||||
dictionaryEffect.install();
|
||||
|
||||
assert.deepEqual(clientInject, ['slots', 'connection', 'locale']);
|
||||
assert.equal(dictionaries[0].namespace, IM_LOCALE_NAMESPACE);
|
||||
assert.deepEqual(Object.keys(dictionaries[0].value.en).sort(), Object.keys(dictionaries[0].value.zh).sort());
|
||||
assert.equal(registrations.length, 1);
|
||||
assert.equal(registrations[0].options.locale, IM_LOCALE_NAMESPACE);
|
||||
assert.equal(registrations[0].options.label(), 'IM bots');
|
||||
|
||||
const markup = renderToStaticMarkup(React.createElement(
|
||||
registrations[0].component,
|
||||
registrations[0].options.inject(),
|
||||
));
|
||||
assert.match(markup, /Connect bots to DeepSeek Harness/);
|
||||
assert.match(markup, />WeChat<|>Feishu<|>DingTalk<|>WeCom</);
|
||||
assert.match(markup, />QQ<[^]*>Slack<[^]*>Telegram<[^]*>Discord<[^]*>WhatsApp</);
|
||||
assert.doesNotMatch(markup, /[\p{Script=Han}]/u);
|
||||
} finally {
|
||||
setImTranslator(null);
|
||||
}
|
||||
});
|
||||
|
||||
test('all nine channel settings and connected cards render English copy', () => {
|
||||
const rpcCall = async () => ({ ok: true, value: {} });
|
||||
const noop = () => {};
|
||||
const account = {
|
||||
botId: 'bot-english',
|
||||
state: 'connected',
|
||||
connected: true,
|
||||
bot: {
|
||||
name: 'Demo Bot',
|
||||
accountIdMasked: 'account••01',
|
||||
appIdMasked: 'app••01',
|
||||
clientIdMasked: 'client••01',
|
||||
idMasked: 'bot••01',
|
||||
username: 'demo_bot',
|
||||
},
|
||||
health: { summary: 'Connection is healthy', lastCheckedAt: '2026-08-16T08:00:00.000Z' },
|
||||
};
|
||||
|
||||
setImTranslator((key) => en[key] ?? key);
|
||||
try {
|
||||
const pages = [
|
||||
WeixinSettingsTab,
|
||||
FeishuSettingsTab,
|
||||
DingtalkSettingsTab,
|
||||
WecomSettingsTab,
|
||||
QqSettingsTab,
|
||||
SlackSettingsTab,
|
||||
TelegramSettingsTab,
|
||||
DiscordSettingsTab,
|
||||
WhatsappSettingsTab,
|
||||
];
|
||||
const pageMarkup = pages.map((Component) =>
|
||||
renderToStaticMarkup(React.createElement(Component, { rpcCall }))).join('\n');
|
||||
assert.match(pageMarkup, /Scan QR code/);
|
||||
assert.match(pageMarkup, /Manual setup/);
|
||||
assert.match(pageMarkup, /Loading WeChat connection status/);
|
||||
assert.match(pageMarkup, /Loading Feishu bots/);
|
||||
assert.match(pageMarkup, /Loading DingTalk connection status/);
|
||||
assert.match(pageMarkup, /Loading WeCom bot status/);
|
||||
assert.match(pageMarkup, /Loading QQ bot status/);
|
||||
assert.match(pageMarkup, /Loading Slack bot status/);
|
||||
assert.match(pageMarkup, /Loading Telegram bot status/);
|
||||
assert.match(pageMarkup, /Loading Discord bot status/);
|
||||
assert.match(pageMarkup, /Loading WhatsApp bot status/);
|
||||
assert.doesNotMatch(pageMarkup, /[\p{Script=Han}]/u);
|
||||
|
||||
const sharedCardProps = {
|
||||
removing: true,
|
||||
onReconnect: noop,
|
||||
onRequestRemove: noop,
|
||||
onConfirmRemove: noop,
|
||||
onCancelRemove: noop,
|
||||
};
|
||||
const cards = [
|
||||
React.createElement(WeixinAccountCard, { ...sharedCardProps, account }),
|
||||
React.createElement(FeishuBotCard, { ...sharedCardProps, connection: account }),
|
||||
React.createElement(DingtalkAccountCard, { ...sharedCardProps, account }),
|
||||
React.createElement(WecomAccountCard, { ...sharedCardProps, account }),
|
||||
React.createElement(QqAccountCard, { ...sharedCardProps, account }),
|
||||
React.createElement(SlackAccountCard, { ...sharedCardProps, account }),
|
||||
React.createElement(TelegramAccountCard, { ...sharedCardProps, account }),
|
||||
React.createElement(DiscordAccountCard, { ...sharedCardProps, account }),
|
||||
React.createElement(WhatsappAccountCard, { ...sharedCardProps, account }),
|
||||
];
|
||||
const cardMarkup = cards.map(renderToStaticMarkup).join('\n');
|
||||
assert.match(cardMarkup, /Connected/);
|
||||
assert.match(cardMarkup, /Message channel/);
|
||||
assert.match(cardMarkup, /Last checked/);
|
||||
assert.match(cardMarkup, /Check connection/);
|
||||
assert.match(cardMarkup, /Remove connection/);
|
||||
assert.doesNotMatch(cardMarkup, /[\p{Script=Han}]/u);
|
||||
} finally {
|
||||
setImTranslator(null);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue