mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-08 09:26:50 +08:00
* feat(channels): add new and clear conversation controls * fix(dingtalk): resolve bot identity before dispatch * feat(chat): expose channel-created chats consistently * refactor(channels): snapshot task delivery routes
293 lines
13 KiB
SQL
293 lines
13 KiB
SQL
-- DingTalk-specific installation identity operations. The underlying channel_*
|
|
-- tables are shared, but these replacement semantics belong to DingTalk's BYO
|
|
-- AppKey model and deliberately stay out of the shared channel query surface.
|
|
|
|
-- name: ListDingTalkUserBindingsForMember :many
|
|
-- Returns only the requesting Multica member's DingTalk identities. The
|
|
-- installation list is member-visible, so returning every member's staff id
|
|
-- here would expose staff ID values more broadly than necessary.
|
|
SELECT installation_id, channel_user_id
|
|
FROM channel_user_binding
|
|
WHERE workspace_id = sqlc.arg(workspace_id)
|
|
AND multica_user_id = sqlc.arg(multica_user_id)
|
|
AND channel_type = 'dingtalk'
|
|
ORDER BY bound_at DESC, id ASC;
|
|
|
|
-- name: LockDingTalkInstallationOwner :exec
|
|
-- Serializes install / replacement decisions for one logical
|
|
-- (workspace, agent, channel) slot. A different-AppKey replacement deletes the
|
|
-- old row and inserts a fresh installation id; the advisory lock closes the
|
|
-- gap where two concurrent replacements could otherwise miss each other's new
|
|
-- row and update it in place, carrying identity state across robot boundaries.
|
|
SELECT pg_advisory_xact_lock(
|
|
hashtextextended(
|
|
(sqlc.arg(workspace_id)::uuid)::text || ':' ||
|
|
(sqlc.arg(agent_id)::uuid)::text || ':dingtalk',
|
|
0
|
|
)
|
|
);
|
|
|
|
-- name: GetDingTalkInstallationOwnerForUpdate :one
|
|
-- Reads the current robot identity after LockDingTalkInstallationOwner has
|
|
-- serialized the logical owner slot. app_id is non-null for every DingTalk
|
|
-- installation; COALESCE treats malformed legacy config as a different robot,
|
|
-- which safely replaces it instead of preserving unknown identity state.
|
|
SELECT id, COALESCE(config ->> 'app_id', '')::text AS app_id
|
|
FROM channel_installation
|
|
WHERE workspace_id = sqlc.arg(workspace_id)
|
|
AND agent_id = sqlc.arg(agent_id)
|
|
AND channel_type = 'dingtalk'
|
|
FOR UPDATE;
|
|
|
|
-- name: UpsertDingTalkBotIdentity :one
|
|
-- Bot identity and the optional permission issue belong to the installation,
|
|
-- not to any one group. Keep this as a separate command so the mixed-version
|
|
-- compatibility trigger can observe the completed command state when the
|
|
-- group-presence command runs next.
|
|
WITH target AS MATERIALIZED (
|
|
SELECT channel_installation.id, channel_installation.workspace_id
|
|
FROM channel_installation
|
|
WHERE channel_installation.id = sqlc.arg(installation_id)
|
|
AND channel_installation.workspace_id = sqlc.arg(workspace_id)
|
|
AND channel_installation.channel_type = 'dingtalk'
|
|
AND channel_installation.status = 'active'
|
|
FOR UPDATE
|
|
), prior_identity AS (
|
|
SELECT
|
|
COALESCE(identity.bot_name, '')::text AS bot_name,
|
|
COALESCE(identity.bot_identity_issue, '')::text AS bot_identity_issue
|
|
FROM target
|
|
LEFT JOIN dingtalk_bot_identity identity ON identity.installation_id = target.id
|
|
), upserted_identity AS (
|
|
INSERT INTO dingtalk_bot_identity (
|
|
workspace_id, installation_id, bot_name, bot_identity_issue
|
|
)
|
|
SELECT
|
|
target.workspace_id,
|
|
target.id,
|
|
COALESCE(NULLIF(sqlc.arg(bot_name)::text, ''), prior_identity.bot_name),
|
|
CASE
|
|
WHEN sqlc.arg(bot_identity_issue)::text <> '' THEN sqlc.arg(bot_identity_issue)::text
|
|
WHEN sqlc.arg(bot_name)::text <> '' THEN ''
|
|
ELSE prior_identity.bot_identity_issue
|
|
END
|
|
FROM target, prior_identity
|
|
ON CONFLICT (installation_id) DO UPDATE SET
|
|
workspace_id = EXCLUDED.workspace_id,
|
|
bot_name = EXCLUDED.bot_name,
|
|
bot_identity_issue = EXCLUDED.bot_identity_issue,
|
|
updated_at = now()
|
|
WHERE dingtalk_bot_identity.workspace_id IS DISTINCT FROM EXCLUDED.workspace_id
|
|
OR dingtalk_bot_identity.bot_name IS DISTINCT FROM EXCLUDED.bot_name
|
|
OR dingtalk_bot_identity.bot_identity_issue IS DISTINCT FROM EXCLUDED.bot_identity_issue
|
|
RETURNING installation_id
|
|
)
|
|
SELECT target.id FROM target;
|
|
|
|
-- name: RecordDingTalkGroupPresence :one
|
|
-- Records an observed bot/group pair without carrying routing state. The
|
|
-- installation identity is copied only for compatibility with the preceding
|
|
-- draft; readers use dingtalk_bot_identity as the authority.
|
|
WITH target AS MATERIALIZED (
|
|
SELECT channel_installation.id, channel_installation.workspace_id
|
|
FROM channel_installation
|
|
WHERE channel_installation.id = sqlc.arg(installation_id)
|
|
AND channel_installation.workspace_id = sqlc.arg(workspace_id)
|
|
AND channel_installation.channel_type = 'dingtalk'
|
|
AND channel_installation.status = 'active'
|
|
FOR KEY SHARE
|
|
), current_identity AS (
|
|
SELECT identity.installation_id, identity.bot_name, identity.bot_identity_issue
|
|
FROM target
|
|
JOIN dingtalk_bot_identity identity ON identity.installation_id = target.id
|
|
), observed AS (
|
|
INSERT INTO dingtalk_group_presence (
|
|
workspace_id, installation_id, conversation_id, conversation_title,
|
|
bot_name, bot_identity_issue
|
|
)
|
|
SELECT
|
|
target.workspace_id,
|
|
target.id,
|
|
sqlc.arg(conversation_id)::text,
|
|
sqlc.arg(conversation_title)::text,
|
|
current_identity.bot_name,
|
|
current_identity.bot_identity_issue
|
|
FROM target
|
|
JOIN current_identity ON current_identity.installation_id = target.id
|
|
ON CONFLICT (installation_id, conversation_id) DO UPDATE SET
|
|
conversation_title = CASE
|
|
WHEN EXCLUDED.conversation_title <> '' THEN EXCLUDED.conversation_title
|
|
ELSE dingtalk_group_presence.conversation_title
|
|
END,
|
|
bot_name = EXCLUDED.bot_name,
|
|
bot_identity_issue = EXCLUDED.bot_identity_issue,
|
|
updated_at = now()
|
|
WHERE (
|
|
dingtalk_group_presence.conversation_title IS DISTINCT FROM EXCLUDED.conversation_title
|
|
AND EXCLUDED.conversation_title <> ''
|
|
)
|
|
OR dingtalk_group_presence.bot_name IS DISTINCT FROM EXCLUDED.bot_name
|
|
OR dingtalk_group_presence.bot_identity_issue IS DISTINCT FROM EXCLUDED.bot_identity_issue
|
|
RETURNING installation_id
|
|
), synced_identity AS (
|
|
UPDATE dingtalk_group_presence presence
|
|
SET
|
|
bot_name = current_identity.bot_name,
|
|
bot_identity_issue = current_identity.bot_identity_issue,
|
|
updated_at = now()
|
|
FROM current_identity
|
|
WHERE presence.installation_id = current_identity.installation_id
|
|
AND presence.conversation_id <> sqlc.arg(conversation_id)::text
|
|
AND (
|
|
presence.bot_name IS DISTINCT FROM current_identity.bot_name
|
|
OR presence.bot_identity_issue IS DISTINCT FROM current_identity.bot_identity_issue
|
|
)
|
|
)
|
|
SELECT target.id FROM target;
|
|
|
|
-- name: RecordDingTalkGroupActivity :one
|
|
-- Advances activity only after the deduplicated inbound message is durable.
|
|
-- Presence observation runs first, so a forgotten group is recreated before
|
|
-- this update executes.
|
|
UPDATE dingtalk_group_presence
|
|
SET last_active_at = now(), mention_count = mention_count + 1, updated_at = now()
|
|
WHERE installation_id = sqlc.arg(installation_id)
|
|
AND conversation_id = sqlc.arg(conversation_id)
|
|
RETURNING installation_id;
|
|
|
|
-- name: ListDingTalkGroupPresencesByWorkspace :many
|
|
-- Active rows are the default inventory. Inactive rows are loaded only when a
|
|
-- client expands one installation's historical section.
|
|
SELECT
|
|
presence.conversation_id,
|
|
presence.conversation_title,
|
|
presence.installation_id,
|
|
installation.agent_id,
|
|
COALESCE(identity.bot_name, presence.bot_name)::text AS bot_name,
|
|
COALESCE(identity.bot_identity_issue, presence.bot_identity_issue)::text AS bot_identity_issue,
|
|
presence.last_active_at,
|
|
presence.mention_count
|
|
FROM dingtalk_group_presence presence
|
|
JOIN channel_installation installation ON installation.id = presence.installation_id
|
|
LEFT JOIN dingtalk_bot_identity identity ON identity.installation_id = presence.installation_id
|
|
WHERE installation.workspace_id = sqlc.arg(workspace_id)
|
|
AND installation.channel_type = 'dingtalk'
|
|
AND installation.status = 'active'
|
|
AND (NOT sqlc.arg(filter_by_agent)::boolean OR installation.agent_id = sqlc.arg(agent_id))
|
|
AND (NOT sqlc.arg(filter_by_installation)::boolean OR installation.id = sqlc.arg(filter_installation_id))
|
|
AND (
|
|
(NOT sqlc.arg(include_inactive)::boolean AND presence.last_active_at >= sqlc.arg(active_since))
|
|
OR (sqlc.arg(include_inactive)::boolean AND (presence.last_active_at < sqlc.arg(active_since) OR presence.last_active_at IS NULL))
|
|
)
|
|
ORDER BY presence.last_active_at DESC NULLS LAST, presence.conversation_title ASC,
|
|
presence.conversation_id ASC, installation.installed_at ASC, installation.id ASC
|
|
LIMIT NULLIF(sqlc.arg(page_limit)::integer, 0)
|
|
OFFSET sqlc.arg(page_offset)::integer;
|
|
|
|
-- name: CountInactiveDingTalkGroupPresencesByWorkspace :many
|
|
SELECT presence.installation_id, installation.agent_id, count(*)::bigint AS group_count
|
|
FROM dingtalk_group_presence presence
|
|
JOIN channel_installation installation ON installation.id = presence.installation_id
|
|
WHERE installation.workspace_id = sqlc.arg(workspace_id)
|
|
AND installation.channel_type = 'dingtalk'
|
|
AND installation.status = 'active'
|
|
AND (presence.last_active_at < sqlc.arg(active_since) OR presence.last_active_at IS NULL)
|
|
AND (NOT sqlc.arg(filter_by_agent)::boolean OR installation.agent_id = sqlc.arg(agent_id))
|
|
GROUP BY presence.installation_id, installation.agent_id;
|
|
|
|
-- name: ListDingTalkBotIdentitiesByWorkspace :many
|
|
-- Identity is installation-owned, so forgetting or collapsing groups cannot
|
|
-- change the connection label.
|
|
SELECT
|
|
installation.id AS installation_id,
|
|
installation.agent_id,
|
|
identity.bot_name,
|
|
identity.bot_identity_issue
|
|
FROM channel_installation installation
|
|
JOIN dingtalk_bot_identity identity ON identity.installation_id = installation.id
|
|
WHERE installation.workspace_id = sqlc.arg(workspace_id)
|
|
AND installation.channel_type = 'dingtalk'
|
|
AND installation.status = 'active'
|
|
AND (NOT sqlc.arg(filter_by_agent)::boolean OR installation.agent_id = sqlc.arg(agent_id));
|
|
|
|
-- name: ForgetDingTalkGroupPresence :one
|
|
DELETE FROM dingtalk_group_presence presence
|
|
USING channel_installation installation
|
|
WHERE presence.installation_id = installation.id
|
|
AND presence.workspace_id = sqlc.arg(workspace_id)
|
|
AND presence.installation_id = sqlc.arg(installation_id)
|
|
AND presence.conversation_id = sqlc.arg(conversation_id)
|
|
AND installation.workspace_id = sqlc.arg(workspace_id)
|
|
AND installation.channel_type = 'dingtalk'
|
|
RETURNING presence.installation_id;
|
|
|
|
-- name: DeleteDingTalkInstallationForReplacement :one
|
|
-- Retires an installation when the same agent is connected with a DIFFERENT
|
|
-- AppKey. A senderStaffId is scoped to one DingTalk organization, so none of the
|
|
-- old installation's identity, token, session, dedup, or outbound state may
|
|
-- cross into the new robot. The caller inserts a fresh installation in the same
|
|
-- transaction, giving the replacement a new installation_id that also fences
|
|
-- late writes and replies from the old connection.
|
|
--
|
|
-- Chat sessions themselves remain as history, but their channel bindings and
|
|
-- outbound cards are removed. Audit and media-intent rows remain useful for
|
|
-- diagnostics / reconciliation, so their installation references are detached.
|
|
WITH retired AS (
|
|
DELETE FROM channel_installation ci
|
|
WHERE ci.id = sqlc.arg(installation_id)
|
|
AND ci.workspace_id = sqlc.arg(workspace_id)
|
|
AND ci.agent_id = sqlc.arg(agent_id)
|
|
AND ci.channel_type = 'dingtalk'
|
|
RETURNING ci.id
|
|
),
|
|
cleared_group_presence AS (
|
|
DELETE FROM dingtalk_group_presence
|
|
WHERE installation_id IN (SELECT id FROM retired)
|
|
),
|
|
cleared_bot_identity AS (
|
|
DELETE FROM dingtalk_bot_identity
|
|
WHERE installation_id IN (SELECT id FROM retired)
|
|
),
|
|
cleared_group_routes AS (
|
|
DELETE FROM dingtalk_group_route
|
|
WHERE installation_id IN (SELECT id FROM retired)
|
|
),
|
|
cleared_task_deliveries AS (
|
|
DELETE FROM channel_task_delivery
|
|
WHERE installation_id IN (SELECT id FROM retired)
|
|
),
|
|
cleared_outbound_messages AS (
|
|
DELETE FROM channel_outbound_message
|
|
WHERE installation_id IN (SELECT id FROM retired)
|
|
),
|
|
cleared_chat_sessions AS (
|
|
DELETE FROM channel_chat_session_binding
|
|
WHERE installation_id IN (SELECT id FROM retired)
|
|
RETURNING chat_session_id
|
|
),
|
|
cleared_outbound_cards AS (
|
|
DELETE FROM channel_outbound_card_message
|
|
WHERE chat_session_id IN (SELECT chat_session_id FROM cleared_chat_sessions)
|
|
),
|
|
cleared_binding_tokens AS (
|
|
DELETE FROM channel_binding_token
|
|
WHERE installation_id IN (SELECT id FROM retired)
|
|
),
|
|
cleared_user_bindings AS (
|
|
DELETE FROM channel_user_binding
|
|
WHERE installation_id IN (SELECT id FROM retired)
|
|
),
|
|
cleared_inbound_dedup AS (
|
|
DELETE FROM channel_inbound_message_dedup
|
|
WHERE installation_id IN (SELECT id FROM retired)
|
|
),
|
|
detached_audit AS (
|
|
UPDATE channel_inbound_audit SET installation_id = NULL
|
|
WHERE installation_id IN (SELECT id FROM retired)
|
|
),
|
|
detached_media_intents AS (
|
|
UPDATE channel_media_pending_object SET installation_id = NULL
|
|
WHERE installation_id IN (SELECT id FROM retired)
|
|
)
|
|
SELECT retired.id FROM retired;
|