mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 01:53:21 +08:00
Improve WhatsApp delivery suggestions and peer labels (ops.30).
Surface chat/access-list contacts as selectable targets with clearer nicknames, and keep conversation peers usable for ops schedulers. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
e84630565c
commit
356538ac18
19 changed files with 827 additions and 324 deletions
|
|
@ -20,8 +20,8 @@ const CHANNEL_LABELS = Object.freeze({
|
|||
});
|
||||
|
||||
/**
|
||||
* Session-header chip: channel peer for IM-bound conversations.
|
||||
* Renders nothing when the current session is not bound to a channel chat.
|
||||
* Session-header chip: channel peer for IM conversations (live or formerly bound).
|
||||
* Renders nothing when the session has no channel provenance.
|
||||
* @param {{
|
||||
* sessionId: string,
|
||||
* resolveChannelPeer?: (sessionId: string, signal?: AbortSignal) => Promise<object|null>,
|
||||
|
|
@ -67,7 +67,7 @@ export function ChannelPeerLabel({ sessionId, resolveChannelPeer }) {
|
|||
|
||||
/**
|
||||
* Resolve the channel peer for a session via WhatsApp (ops-first).
|
||||
* Returns null when the session is not bound to a WhatsApp conversation.
|
||||
* Works for live bindings and sessions unbound by `/new` (provenance retained).
|
||||
* @param {(endpoint: string, payload: object, signal?: AbortSignal) => Promise<unknown>} whatsappRpcCall
|
||||
* @param {string} sessionId
|
||||
* @param {AbortSignal} [signal]
|
||||
|
|
|
|||
|
|
@ -325,7 +325,7 @@ function TargetForm({
|
|||
value: name,
|
||||
disabled: testing,
|
||||
maxLength: 80,
|
||||
placeholder: '例如 每日汇报群',
|
||||
placeholder: definition.label === 'WhatsApp' ? '例如 张三 · 13800138000' : '例如 每日汇报群',
|
||||
onChange: (event) => setName(event.target.value),
|
||||
})),
|
||||
h('label', { className: 'dim-targetField' },
|
||||
|
|
@ -386,13 +386,12 @@ function TargetForm({
|
|||
}
|
||||
|
||||
function suggestionOptionLabel(definition, suggestion, added) {
|
||||
const name = suggestionDisplayName(suggestion);
|
||||
const name = suggestionDisplayName(suggestion) || suggestionFallbackName(definition, suggestion);
|
||||
const type = localizeText(kindLabel(definition, suggestion.kind));
|
||||
const route = fieldsFor(definition, suggestion.kind)
|
||||
.map((field) => maskRouteValue(suggestion.route?.[field.key]))
|
||||
.filter(Boolean)
|
||||
.join(' · ');
|
||||
return [name, type, route, added ? localizeText('已添加') : null]
|
||||
const firstField = fieldsFor(definition, suggestion.kind)[0];
|
||||
const route = firstField ? String(suggestion.route?.[firstField.key] ?? '').trim() : '';
|
||||
const showRoute = route && !name.includes(route) && !name.includes(route.split('@')[0] || '');
|
||||
return [name, type, showRoute ? maskRouteValue(route) : null, added ? localizeText('已添加') : null]
|
||||
.filter(Boolean)
|
||||
.join(' · ');
|
||||
}
|
||||
|
|
@ -409,12 +408,15 @@ function TargetSuggestionPicker({
|
|||
onCancel,
|
||||
}) {
|
||||
const configured = new Set(targets.map((target) => routeIdentity(definition, target)).filter(Boolean));
|
||||
const channelHint = definition.label === 'WhatsApp'
|
||||
? '会列出已聊过的私聊/群聊,以及访问名单里记住的联系人与群(显示为昵称 · 电话 / 群名)。'
|
||||
: '选择后会自动填写目标信息和调用别名,确认后再保存。';
|
||||
|
||||
return h('section', { className: 'dim-targetSuggestions', 'aria-label': '从已聊过的会话选择' },
|
||||
h('div', { className: 'dim-targetSuggestionHeading' },
|
||||
h('div', null,
|
||||
h('h3', null, '从已聊过的会话选择'),
|
||||
h('p', null, '选择后会自动填写目标信息和调用别名,确认后再保存。')),
|
||||
h('p', null, channelHint)),
|
||||
h(DeliveryButton, {
|
||||
onClick: () => void onRefresh(),
|
||||
disabled: phase === 'loading',
|
||||
|
|
@ -428,28 +430,33 @@ function TargetSuggestionPicker({
|
|||
: suggestions.length === 0
|
||||
? h('div', { className: 'dim-targetSuggestionState' },
|
||||
h('strong', null, '还没有可选择的会话'),
|
||||
h('p', null, '先在对应平台与机器人聊一条消息,再刷新。'))
|
||||
: h('label', { className: 'dim-targetSuggestionField' },
|
||||
h('span', null, '已聊会话'),
|
||||
h('select', {
|
||||
name: 'suggestion',
|
||||
value: '',
|
||||
onChange: (event) => {
|
||||
if (event.target.value === '') return;
|
||||
const suggestion = suggestions[Number(event.target.value)];
|
||||
if (suggestion) onSelect(suggestion);
|
||||
},
|
||||
},
|
||||
h('option', { value: '', disabled: true }, '从会话选择targetID'),
|
||||
h('p', null, '先在对应平台与机器人聊一条消息(私聊或群聊),或先在「访问设置」里出现过联系人/群,再刷新。'))
|
||||
: h('ul', { className: 'dim-targetSuggestionList', role: 'listbox', 'aria-label': '可选投递会话' },
|
||||
suggestions.map((suggestion, index) => {
|
||||
const identity = routeIdentity(definition, suggestion);
|
||||
const added = configured.has(identity);
|
||||
return React.createElement('option', {
|
||||
key: suggestion.id ?? suggestion.suggestionId ?? `${identity}:${index}`,
|
||||
value: String(index),
|
||||
disabled: added,
|
||||
}, suggestionOptionLabel(definition, suggestion, added));
|
||||
}))),
|
||||
return h('li', { key: suggestion.id ?? suggestion.suggestionId ?? `${identity}:${index}` },
|
||||
h('button', {
|
||||
type: 'button',
|
||||
className: 'dim-targetSuggestionItem',
|
||||
role: 'option',
|
||||
'aria-disabled': added ? 'true' : undefined,
|
||||
disabled: added,
|
||||
onClick: () => { if (!added) onSelect(suggestion); },
|
||||
},
|
||||
// Platform display names must stay raw (bypass h() i18n phrase rewrite).
|
||||
React.createElement('strong', null,
|
||||
suggestionDisplayName(suggestion)
|
||||
|| suggestionFallbackName(definition, suggestion)),
|
||||
h('span', null, [
|
||||
localizeText(kindLabel(definition, suggestion.kind)),
|
||||
added ? localizeText('已添加') : null,
|
||||
].filter(Boolean).join(' · ')),
|
||||
React.createElement('code', null, fieldsFor(definition, suggestion.kind)
|
||||
.map((field) => maskRouteValue(suggestion.route?.[field.key]))
|
||||
.filter(Boolean)
|
||||
.join(' · '))));
|
||||
})),
|
||||
h('div', { className: 'dim-targetFormActions' },
|
||||
h(DeliveryButton, { onClick: onCancel }, '取消'),
|
||||
h(DeliveryButton, { onClick: onManual }, '手动填写(高级)')));
|
||||
|
|
|
|||
|
|
@ -903,12 +903,16 @@ const EN = Object.freeze({
|
|||
'已聊会话': 'Conversation',
|
||||
'从会话选择targetID': 'Select a targetID from conversations',
|
||||
'选择后会自动填写目标信息和调用别名,确认后再保存。': 'Selecting a conversation fills the target details and call alias. Review them before saving.',
|
||||
'会列出已聊过的私聊/群聊,以及访问名单里记住的联系人与群(显示为昵称 · 电话 / 群名)。': 'Lists known DMs and groups, plus remembered access-list contacts and groups (shown as nickname · phone / group name).',
|
||||
'可选投递会话': 'Available delivery conversations',
|
||||
'例如 张三 · 13800138000': 'For example, Alice · 13800138000',
|
||||
'正在刷新…': 'Refreshing…',
|
||||
'刷新': 'Refresh',
|
||||
'正在读取已聊会话…': 'Loading known conversations…',
|
||||
'无法读取已聊会话,请稍后重试。': 'Could not load known conversations. Try again later.',
|
||||
'还没有可选择的会话': 'No conversations to choose from',
|
||||
'先在对应平台与机器人聊一条消息,再刷新。': 'Send the bot a message on the platform, then refresh.',
|
||||
'先在对应平台与机器人聊一条消息(私聊或群聊),或先在「访问设置」里出现过联系人/群,再刷新。': 'Chat with the bot first (DM or group), or wait until contacts/groups appear under Access settings, then refresh.',
|
||||
'已添加': 'Added',
|
||||
'手动填写(高级)': 'Enter manually (advanced)',
|
||||
'测试消息已发送,请到目标会话确认。': 'Test message sent. Confirm it in the target conversation.',
|
||||
|
|
|
|||
|
|
@ -425,6 +425,13 @@ body[data-ds-dark-theme] .dim-page select option, body[data-ds-dark-theme] .dim-
|
|||
.dim-targetSuggestionField { min-width: 0; display: grid; gap: 5px; margin-top: 12px; color: var(--dsw-alias-label-secondary, #646a73); font-size: 11px; line-height: 16px; }
|
||||
.dim-targetSuggestionField select { width: 100%; min-width: 0; height: 38px; padding: 0 10px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 8px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-1, #fff); font: inherit; font-size: 12px; cursor: pointer; }
|
||||
.dim-targetSuggestionField select:focus { outline: 2px solid color-mix(in srgb, var(--dsw-alias-state-business-primary, #3370ff) 28%, transparent); border-color: var(--dsw-alias-state-business-primary, #3370ff); }
|
||||
.dim-targetSuggestionList { list-style: none; margin: 12px 0 0; padding: 0; display: grid; gap: 8px; max-height: 280px; overflow: auto; }
|
||||
.dim-targetSuggestionItem { width: 100%; box-sizing: border-box; display: grid; gap: 2px; text-align: left; padding: 10px 12px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 8px; background: var(--dsw-alias-bg-layer-1, #fff); color: inherit; font: inherit; cursor: pointer; }
|
||||
.dim-targetSuggestionItem:hover:not(:disabled) { border-color: var(--dsw-alias-state-business-primary, #3370ff); }
|
||||
.dim-targetSuggestionItem:disabled { opacity: 0.55; cursor: not-allowed; }
|
||||
.dim-targetSuggestionItem strong { color: var(--dsw-alias-label-primary, #1f2329); font-size: 13px; line-height: 20px; font-weight: 600; }
|
||||
.dim-targetSuggestionItem span { color: var(--dsw-alias-label-secondary, #646a73); font-size: 11px; line-height: 16px; }
|
||||
.dim-targetSuggestionItem code { color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; line-height: 16px; word-break: break-all; }
|
||||
.dim-targetForm { margin-top: 14px; padding: 14px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 10px; background: var(--dsw-alias-bg-module-platform, #f7f8fa); }
|
||||
.dim-targetFormHeading h3 { margin: 0; font-size: 14px; line-height: 21px; }
|
||||
.dim-targetFormHeading p { margin: 2px 0 0; color: var(--dsw-alias-label-secondary, #646a73); font-size: 11px; line-height: 17px; }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue