Files
knowledge-governance/internal/platform/postgres/dingtalk_space.go
T
姚章浩 e60b10256a fix(settings): add DingTalk spaces and render the real login QR
Address two gaps in the first DingTalk-session UI:
- add an explicit admin-only '+ 新增钉钉知识库' action on the data-source
  panel; it accepts an alidocs space URL, resolves the live name/root through
  the authenticated sidecar, writes the inventory root atomically, upserts a
  deterministic source connection, and enqueues its first catalog refresh
- read DingTalk's real QR canvas pixels rather than taking a DOM screenshot;
  select by dark-pixel content, composite onto a white quiet zone, and render
  at 320x320 so the dialog contains a large complete code, not a page thumbnail

The live refresh path resolves registered roots from the connection locator,
while existing name-based DingTalk sources remain compatible.
2026-09-30 10:54:07 +08:00

93 lines
3.5 KiB
Go

package postgres
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"strings"
"time"
"github.com/jackc/pgx/v5"
)
// DingTalk space registration: a knowledge-base space discovered through the
// login sidecar becomes a source_connections row with a DETERMINISTIC id,
// derived the same way the catalog importer derives stable ids
// ('sc-' + sha256('dingtalk-'+root_key)[:32]). Re-registering the same space
// is an idempotent re-sync of name/root, never a duplicate row. New rows are
// enabled but carry no schedule (next_refresh_at stays NULL): automatic scans
// begin only after an admin sets an interval — same posture as the existing
// imported DingTalk sources.
// RegisterDingtalkSpaceInput describes one space resolved by the sidecar.
type RegisterDingtalkSpaceInput struct {
RootKey string // sidecar inventory key, e.g. "atom" or "space-1f2e3d4c5b"
Name string // live space name from DingTalk spaceProfile
SpaceID string // alidocs space id from the URL
RootID string // root dentry uuid
Now time.Time
}
func (in RegisterDingtalkSpaceInput) ConnectionID() string {
sum := sha256.Sum256([]byte("dingtalk-" + in.RootKey))
return "sc-" + hex.EncodeToString(sum[:])[:32]
}
func (in RegisterDingtalkSpaceInput) validate() error {
if in.Now.IsZero() {
return errors.New("dingtalk space registration: now is required")
}
if !validShortID(in.RootKey) || strings.TrimSpace(in.Name) == "" || len(in.Name) > 255 {
return errors.New("dingtalk space registration: invalid key or name")
}
for _, id := range []string{in.SpaceID, in.RootID} {
if id == "" || len(id) > 128 || strings.ContainsAny(id, " \t\r\n/") {
return errors.New("dingtalk space registration: invalid identifier")
}
}
return nil
}
func (s *Store) RegisterDingtalkSpace(ctx context.Context, in RegisterDingtalkSpaceInput) (string, error) {
if err := in.validate(); err != nil {
return "", err
}
connectionID := in.ConnectionID()
var existingName, existingRoot string
err := s.db.QueryRow(ctx, `SELECT name, base_url FROM source_connections WHERE id = $1`, connectionID).Scan(&existingName, &existingRoot)
switch {
case errors.Is(err, pgx.ErrNoRows):
// fresh registration below
case err != nil:
return "", fmt.Errorf("load dingtalk connection: %w", err)
default:
// A different space resolved under the same key must not silently
// rewrite the existing connection.
if !strings.HasPrefix(existingRoot, "spaces/"+in.RootID+"?") {
return "", fmt.Errorf("dingtalk space registration: connection %s already serves another root", connectionID)
}
}
// base_url stores a stable, non-secret locator (the root dentry uuid plus
// the space id) so re-registration can verify identity and the UI can
// explain what a DingTalk connection points at.
locator := "spaces/" + in.RootID + "?space=" + in.SpaceID
_, err = s.db.Exec(ctx, `
INSERT INTO source_connections (
id, source_type, name, base_url, secret_ref, enabled,
schedule, last_health, created_at, updated_at, updated_by
) VALUES ($1, 'dingtalk', $2, $3, '', TRUE, '{}'::jsonb, '{}'::jsonb, $4, $4, $5)
ON CONFLICT (id) DO UPDATE
SET name = EXCLUDED.name, base_url = EXCLUDED.base_url,
updated_at = EXCLUDED.updated_at, updated_by = EXCLUDED.updated_by`,
connectionID, in.Name, locator, in.Now.UTC(), "dingtalk-registration")
if err != nil {
if isUniqueViolation(err) {
return "", fmt.Errorf("dingtalk space registration: name %q already used by another connection", in.Name)
}
return "", fmt.Errorf("register dingtalk space: %w", err)
}
return connectionID, nil
}