Files
multica/server/pkg/db/queries/dingtalk.sql
T
YYClaw 9e2d9b6d1c MUL-6661 feat(channels): add /new and /clear conversation controls (#7468)
* 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
2026-08-25 18:28:01 +08:00

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;