e60b10256a
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.
93 lines
3.5 KiB
Go
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
|
|
}
|