refactor: 后端仓库层收口(A1:课程/产品/素材)+ 收进工作区既有对象化重构
本提交含两部分。第一部分是本轮工作;第二部分是此前一直留在工作区、
从未提交的对象化重构,与第一部分在文件上互相咬合(internal/repository
整个包都是未跟踪状态,且 api 层已有文件引用它),无法拆成两个可编译的提交。
一、仓库层收口 A1 批(本轮工作)
把 api 层手写的 store.DB 查询收进具名仓库方法,只给真正获益的对象做方法,
不机械包裹全量。本批迁移 22 处裸查询(courses.go 9 / media.go 12 / products.go 1),
新增方法:
- MediaFileRepo.ListByBind / ListForAudit / MarkExtracted
- KnowledgeChunkRepo.CountByMediaFile
- ProductRepo.GetVisibleByID
两条业务口径改由仓库单点持有,避免各处手写漂移:
「只有 approved 素材出现在课程详情」与「已停用产品不在课程详情露出」。
修掉两个真实缺陷:
- ProductRepo.GetByID 缺 Where 条件。此前 GET /api/products/{id} 对任意 id 都返回
第一条产品、对不存在的 id 返回 200,且 PUT /api/products/{id} 会覆盖第一条产品
—— 数据损坏级。全仓扫描确认这是唯一一处同型写法。
- ProductRepo.Delete 写 status="deleted",而 DELETE 处理器文档与回包都声称
"inactive",接口在说谎;管理员用 status=all 拉列表会看到前端不认识的状态。
已对齐为 inactive(与 CourseRepo.Delete 一致)。
删除 8 个零调用且列名不存在的死方法(一调即 SQL 报错):
- media_file 上的 file_path / file_type / approval_status 三列并不存在,
GetByPath / ListByType / UpdateStatus 全废
- knowledge_chunk 上的 space_id 列不存在(模型早已改为 knowledge_space_key),
List / Total / ListBySpaceIDs / DeleteBySpace / SearchByVector 全废
取舍边界:能对当前 schema 跑通的死方法保留,跑不通的删或修。
CourseRepo.List 补齐 status=all 档(此前传给它会当作 status='all' 过滤出空列表)。
该方法此前零调用,现与产品列表语义对齐。
验证:go build ./... 与 go test ./... 全绿;另用真实 HTTP 请求验证 34 项
(课程 17 / 产品 3 / 素材 14),跑在数据库副本与独立 KB_DATA_DIR 上,
含 multipart 真上传 → 审批 → pdftotext 提取 → 分片入库的完整链路。
二、此前未提交的对象化重构(非本轮工作)
- 新增 internal/repository 仓库层、connectors、skills、specialists、xapps、jsonutil,
model/task_record|task_run|task_artifact、api/task_runtime|action_definition|chat_message
- 删除 api/app_definition、connectors、my_app_center、notification、office_skill、
export_docx|pptx|xlsx、official_account_* 等,随 XApp/Skill/Specialist/Connector
可插拔打包方向(AR10/AR11)调整
- 资产目录归位:backend-go/knowledge_source → assets/knowledge/source、
training_materials → assets/training/materials;README 内相对路径同步加深两级;
deploy env 补 ASSET_ROOT_DIR 并改 KNOWLEDGE_SOURCE_DIR / TRAINING_MATERIALS_DIR
- 前端新增 skills/ specialists/ connectors/ xapps/ 目录与对应页面
验证:前端 npm run build 通过(7.26s)。
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
package connectorapi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"eai_agentplatform/backend/internal/config"
|
||||
connectorcontracts "eai_agentplatform/backend/internal/connectors/core/contracts"
|
||||
connectorregistry "eai_agentplatform/backend/internal/connectors/registry"
|
||||
"eai_agentplatform/backend/internal/web"
|
||||
)
|
||||
|
||||
var runtimeCfg *config.Config
|
||||
|
||||
func UseConfig(cfg *config.Config) {
|
||||
runtimeCfg = cfg
|
||||
}
|
||||
|
||||
// ListConnectors GET /api/connectors
|
||||
func ListConnectors(c *gin.Context) {
|
||||
web.OK(c, connectorregistry.ListDefinitions(runtimeCfg))
|
||||
}
|
||||
|
||||
// GetConnector GET /api/connectors/:key
|
||||
func GetConnector(c *gin.Context) {
|
||||
key := strings.TrimSpace(c.Param("key"))
|
||||
if key == "" {
|
||||
web.Fail(c, web.NewBadRequest("连接器 key 不能为空"))
|
||||
return
|
||||
}
|
||||
definition, ok := connectorregistry.GetDefinition(runtimeCfg, key)
|
||||
if !ok {
|
||||
web.Fail(c, web.NewNotFoundError("连接器不存在"))
|
||||
return
|
||||
}
|
||||
web.OK(c, definition)
|
||||
}
|
||||
|
||||
// QueryConnector POST /api/connectors/:key/query
|
||||
func QueryConnector(c *gin.Context) {
|
||||
key := strings.TrimSpace(c.Param("key"))
|
||||
if key == "" {
|
||||
web.Fail(c, web.NewBadRequest("连接器 key 不能为空"))
|
||||
return
|
||||
}
|
||||
|
||||
var req connectorcontracts.QueryRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
web.Fail(c, web.NewBadRequest("请求参数错误"))
|
||||
return
|
||||
}
|
||||
result, err := connectorregistry.Query(c.Request.Context(), runtimeCfg, key, req)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, connectorcontracts.ErrConnectorNotFound):
|
||||
web.Fail(c, web.NewNotFoundError("连接器不存在"))
|
||||
case errors.Is(err, connectorcontracts.ErrConnectorQueryUnsupported):
|
||||
web.Fail(c, web.NewBadRequest("该连接器当前不支持 query,请在动作面板中作为输出连接器使用"))
|
||||
default:
|
||||
web.Fail(c, web.NewBadRequest(err.Error()))
|
||||
}
|
||||
return
|
||||
}
|
||||
web.OK(c, result)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package connectorcore
|
||||
|
||||
type SharedReferencePolicy string
|
||||
|
||||
const (
|
||||
SharedReferenceReadonlyHistory SharedReferencePolicy = "readonly_history"
|
||||
SharedReferenceTombstone SharedReferencePolicy = "convert_to_tombstone"
|
||||
SharedReferenceDetach SharedReferencePolicy = "detach_reference"
|
||||
SharedReferenceBlockUninstall SharedReferencePolicy = "block_uninstall"
|
||||
)
|
||||
|
||||
type UninstallMeta struct {
|
||||
OwnedDefinitionKeys []string
|
||||
OwnedCatalogEntries []string
|
||||
ReferencePolicy SharedReferencePolicy
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package contracts
|
||||
|
||||
import "errors"
|
||||
|
||||
// ErrConnectorNotFound 表示连接器键不存在。
|
||||
var ErrConnectorNotFound = errors.New("connector not found")
|
||||
|
||||
// ErrConnectorQueryUnsupported 表示连接器不支持查询。
|
||||
var ErrConnectorQueryUnsupported = errors.New("connector query unsupported")
|
||||
|
||||
// Definition 描述一个连接器的面向用户定义。
|
||||
type Definition struct {
|
||||
Key string `json:"key"`
|
||||
Label string `json:"label"`
|
||||
Vendor string `json:"vendor"`
|
||||
Category string `json:"category"`
|
||||
Direction string `json:"direction"`
|
||||
Description string `json:"description"`
|
||||
Mode string `json:"mode"`
|
||||
Status string `json:"status"`
|
||||
AuthConfigured bool `json:"auth_configured"`
|
||||
Capabilities []string `json:"capabilities"`
|
||||
Actions []ActionDefinition `json:"actions"`
|
||||
Objects []ObjectDefinition `json:"objects"`
|
||||
}
|
||||
|
||||
// ActionDefinition 描述一个连接器动作。
|
||||
type ActionDefinition struct {
|
||||
Key string `json:"key"`
|
||||
Label string `json:"label"`
|
||||
ActionType string `json:"action_type"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// ObjectDefinition 描述一个连接器可操作的对象。
|
||||
type ObjectDefinition struct {
|
||||
Key string `json:"key"`
|
||||
Label string `json:"label"`
|
||||
Description string `json:"description"`
|
||||
Mode string `json:"mode"`
|
||||
RecommendedFormID string `json:"recommended_form_id"`
|
||||
DefaultFields []string `json:"default_fields"`
|
||||
FilterHint string `json:"filter_hint"`
|
||||
}
|
||||
|
||||
// QueryRequest 描述一次连接器查询请求。
|
||||
type QueryRequest struct {
|
||||
ObjectKey string `json:"object_key"`
|
||||
FormID string `json:"form_id"`
|
||||
FieldKeys []string `json:"field_keys"`
|
||||
FilterString string `json:"filter_string"`
|
||||
OrderString string `json:"order_string"`
|
||||
StartRow int `json:"start_row"`
|
||||
Limit int `json:"limit"`
|
||||
UseDemo bool `json:"use_demo"`
|
||||
Filters map[string]any `json:"filters"`
|
||||
}
|
||||
|
||||
// QueryResult 描述一次连接器查询结果。
|
||||
type QueryResult struct {
|
||||
ConnectorKey string `json:"connector_key"`
|
||||
ConnectorLabel string `json:"connector_label"`
|
||||
ObjectKey string `json:"object_key"`
|
||||
ObjectLabel string `json:"object_label"`
|
||||
Mode string `json:"mode"`
|
||||
Request map[string]any `json:"request"`
|
||||
Outputs map[string]any `json:"outputs"`
|
||||
Artifacts []map[string]any `json:"artifacts"`
|
||||
Citations []map[string]any `json:"citations"`
|
||||
Risks []map[string]any `json:"risks"`
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package connectorcore
|
||||
|
||||
type Manifest struct {
|
||||
Key string
|
||||
Label string
|
||||
OwnedDefinitionKeys []string
|
||||
OwnedCatalogEntries []string
|
||||
ReferencePolicy SharedReferencePolicy
|
||||
}
|
||||
|
||||
func (m Manifest) UninstallMeta() UninstallMeta {
|
||||
return UninstallMeta{
|
||||
OwnedDefinitionKeys: append([]string(nil), m.OwnedDefinitionKeys...),
|
||||
OwnedCatalogEntries: append([]string(nil), m.OwnedCatalogEntries...),
|
||||
ReferencePolicy: m.ReferencePolicy,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package connectorcore
|
||||
|
||||
type Registry struct {
|
||||
manifests []Manifest
|
||||
byKey map[string]Manifest
|
||||
}
|
||||
|
||||
func NewRegistry(manifests ...Manifest) *Registry {
|
||||
items := make([]Manifest, 0, len(manifests))
|
||||
byKey := make(map[string]Manifest, len(manifests))
|
||||
for _, manifest := range manifests {
|
||||
if manifest.Key == "" {
|
||||
continue
|
||||
}
|
||||
items = append(items, manifest)
|
||||
byKey[manifest.Key] = manifest
|
||||
}
|
||||
return &Registry{manifests: items, byKey: byKey}
|
||||
}
|
||||
|
||||
func (r *Registry) Manifests() []Manifest {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
items := make([]Manifest, len(r.manifests))
|
||||
copy(items, r.manifests)
|
||||
return items
|
||||
}
|
||||
|
||||
func (r *Registry) ByKey(key string) (Manifest, bool) {
|
||||
if r == nil {
|
||||
return Manifest{}, false
|
||||
}
|
||||
item, ok := r.byKey[key]
|
||||
return item, ok
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package connectorcore
|
||||
|
||||
type UninstallPreview struct {
|
||||
DefinitionKeys []string
|
||||
CatalogEntries []string
|
||||
Policy SharedReferencePolicy
|
||||
}
|
||||
|
||||
func BuildUninstallPreview(manifest Manifest) UninstallPreview {
|
||||
meta := manifest.UninstallMeta()
|
||||
return UninstallPreview{
|
||||
DefinitionKeys: meta.OwnedDefinitionKeys,
|
||||
CatalogEntries: meta.OwnedCatalogEntries,
|
||||
Policy: meta.ReferencePolicy,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,564 @@
|
||||
package kingdee
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"eai_agentplatform/backend/internal/config"
|
||||
connectorcontracts "eai_agentplatform/backend/internal/connectors/core/contracts"
|
||||
connectorquery "eai_agentplatform/backend/internal/connectors/query"
|
||||
)
|
||||
|
||||
type Connector struct {
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
func New(cfg *config.Config) *Connector {
|
||||
return &Connector{cfg: cfg}
|
||||
}
|
||||
|
||||
func (k *Connector) Definition() connectorcontracts.Definition {
|
||||
mode := "demo"
|
||||
status := "not_configured"
|
||||
if k.isConfigured() {
|
||||
mode = "live"
|
||||
status = "ready"
|
||||
}
|
||||
|
||||
objects := make([]connectorcontracts.ObjectDefinition, 0, len(kingdeeObjectCatalog))
|
||||
for _, item := range kingdeeObjectOrder {
|
||||
obj := kingdeeObjectCatalog[item]
|
||||
objects = append(objects, obj)
|
||||
}
|
||||
|
||||
return connectorcontracts.Definition{
|
||||
Key: "kingdee",
|
||||
Label: "金蝶云星空读取连接器",
|
||||
Vendor: "Kingdee",
|
||||
Category: "erp",
|
||||
Direction: "input",
|
||||
Description: "面向 DW / ADW 的金蝶只读连接器,优先支持 ExecuteBillQuery 拉取主数据与业务单据。",
|
||||
Mode: mode,
|
||||
Status: status,
|
||||
AuthConfigured: k.isConfigured(),
|
||||
Capabilities: []string{"read", "query", "schema_hint"},
|
||||
Actions: []connectorcontracts.ActionDefinition{
|
||||
{Key: "fetch_customer", Label: "读取客户", ActionType: "FetchCustomer", Description: "从金蝶读取客户主数据"},
|
||||
{Key: "fetch_material", Label: "读取物料", ActionType: "FetchMaterial", Description: "从金蝶读取物料主数据"},
|
||||
{Key: "fetch_sales_order", Label: "读取销售订单", ActionType: "FetchSalesOrder", Description: "从金蝶读取销售订单"},
|
||||
{Key: "fetch_purchase_order", Label: "读取采购订单", ActionType: "FetchPurchaseOrder", Description: "从金蝶读取采购订单"},
|
||||
{Key: "query_business_document", Label: "查询业务单据", ActionType: "QueryBusinessDocument", Description: "按表单和字段查询金蝶业务对象"},
|
||||
},
|
||||
Objects: objects,
|
||||
}
|
||||
}
|
||||
|
||||
func (k *Connector) Query(ctx context.Context, req connectorcontracts.QueryRequest) (connectorcontracts.QueryResult, error) {
|
||||
objectDef, err := k.resolveObject(req.ObjectKey, req.FormID)
|
||||
if err != nil {
|
||||
return connectorcontracts.QueryResult{}, err
|
||||
}
|
||||
|
||||
req.ObjectKey = objectDef.Key
|
||||
req.FormID = connectorquery.FirstNonEmpty(req.FormID, objectDef.RecommendedFormID)
|
||||
req.FieldKeys = connectorquery.NormalizeFieldKeys(req.FieldKeys, objectDef.DefaultFields)
|
||||
req.Limit = connectorquery.NormalizeLimit(req.Limit)
|
||||
if req.StartRow < 0 {
|
||||
req.StartRow = 0
|
||||
}
|
||||
|
||||
if req.UseDemo || !k.isConfigured() {
|
||||
return k.queryDemo(objectDef, req), nil
|
||||
}
|
||||
return k.queryLive(ctx, objectDef, req)
|
||||
}
|
||||
|
||||
func (k *Connector) queryDemo(objectDef connectorcontracts.ObjectDefinition, req connectorcontracts.QueryRequest) connectorcontracts.QueryResult {
|
||||
sourceRows := connectorquery.CloneRows(kingdeeDemoData[objectDef.Key])
|
||||
records := connectorquery.SliceRows(sourceRows, req.StartRow, req.Limit)
|
||||
if len(req.FieldKeys) > 0 {
|
||||
records = connectorquery.SelectFields(records, req.FieldKeys)
|
||||
}
|
||||
|
||||
risks := []map[string]any{
|
||||
{
|
||||
"level": "low",
|
||||
"code": "demo_mode",
|
||||
"message": "当前使用内置演示数据,未直连真实金蝶环境。",
|
||||
},
|
||||
}
|
||||
if strings.TrimSpace(req.FilterString) != "" {
|
||||
risks = append(risks, map[string]any{
|
||||
"level": "low",
|
||||
"code": "filter_not_applied",
|
||||
"message": "演示模式未执行 FilterString,请以 live 模式验证过滤条件。",
|
||||
})
|
||||
}
|
||||
|
||||
return connectorcontracts.QueryResult{
|
||||
ConnectorKey: "kingdee",
|
||||
ConnectorLabel: "金蝶云星空读取连接器",
|
||||
ObjectKey: objectDef.Key,
|
||||
ObjectLabel: objectDef.Label,
|
||||
Mode: "demo",
|
||||
Request: connectorquery.BuildRequestEcho(req),
|
||||
Outputs: map[string]any{
|
||||
"form_id": req.FormID,
|
||||
"field_keys": req.FieldKeys,
|
||||
"total": len(sourceRows),
|
||||
"count": len(records),
|
||||
"records": records,
|
||||
},
|
||||
Artifacts: []map[string]any{},
|
||||
Citations: []map[string]any{
|
||||
{
|
||||
"title": fmt.Sprintf("金蝶演示对象 %s", objectDef.Label),
|
||||
"source": "builtin_demo_dataset",
|
||||
"connector": "kingdee",
|
||||
"fetched_at": time.Now().Format(time.RFC3339),
|
||||
},
|
||||
},
|
||||
Risks: risks,
|
||||
}
|
||||
}
|
||||
|
||||
func (k *Connector) queryLive(ctx context.Context, objectDef connectorcontracts.ObjectDefinition, req connectorcontracts.QueryRequest) (connectorcontracts.QueryResult, error) {
|
||||
client, err := k.newHTTPClient()
|
||||
if err != nil {
|
||||
return connectorcontracts.QueryResult{}, err
|
||||
}
|
||||
if err := k.login(ctx, client); err != nil {
|
||||
return connectorcontracts.QueryResult{}, err
|
||||
}
|
||||
|
||||
records, err := k.executeBillQuery(ctx, client, req)
|
||||
if err != nil {
|
||||
return connectorcontracts.QueryResult{}, err
|
||||
}
|
||||
|
||||
return connectorcontracts.QueryResult{
|
||||
ConnectorKey: "kingdee",
|
||||
ConnectorLabel: "金蝶云星空读取连接器",
|
||||
ObjectKey: objectDef.Key,
|
||||
ObjectLabel: objectDef.Label,
|
||||
Mode: "live",
|
||||
Request: connectorquery.BuildRequestEcho(req),
|
||||
Outputs: map[string]any{
|
||||
"form_id": req.FormID,
|
||||
"field_keys": req.FieldKeys,
|
||||
"count": len(records),
|
||||
"records": records,
|
||||
},
|
||||
Artifacts: []map[string]any{},
|
||||
Citations: []map[string]any{
|
||||
{
|
||||
"title": fmt.Sprintf("金蝶实时对象 %s", objectDef.Label),
|
||||
"source": sanitizeKingdeeBaseURL(k.cfg.KingdeeBaseURL),
|
||||
"connector": "kingdee",
|
||||
"fetched_at": time.Now().Format(time.RFC3339),
|
||||
},
|
||||
},
|
||||
Risks: []map[string]any{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (k *Connector) newHTTPClient() (*http.Client, error) {
|
||||
jar, err := cookiejar.New(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
timeoutSec := k.cfg.KingdeeTimeoutSec
|
||||
if timeoutSec <= 0 {
|
||||
timeoutSec = 15
|
||||
}
|
||||
return &http.Client{
|
||||
Timeout: time.Duration(timeoutSec) * time.Second,
|
||||
Jar: jar,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (k *Connector) login(ctx context.Context, client *http.Client) error {
|
||||
loginURL := sanitizeKingdeeBaseURL(k.cfg.KingdeeBaseURL) + "Kingdee.BOS.WebApi.ServicesStub.AuthService.ValidateUser.common.kdsvc"
|
||||
payloads := []any{
|
||||
map[string]any{
|
||||
"acctid": k.cfg.KingdeeAccountID,
|
||||
"username": k.cfg.KingdeeUsername,
|
||||
"password": k.cfg.KingdeePassword,
|
||||
"lcid": k.cfg.KingdeeLCID,
|
||||
},
|
||||
map[string]any{
|
||||
"acctID": k.cfg.KingdeeAccountID,
|
||||
"username": k.cfg.KingdeeUsername,
|
||||
"password": k.cfg.KingdeePassword,
|
||||
"lcid": k.cfg.KingdeeLCID,
|
||||
},
|
||||
[]any{k.cfg.KingdeeAccountID, k.cfg.KingdeeUsername, k.cfg.KingdeePassword, k.cfg.KingdeeLCID},
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
for _, payload := range payloads {
|
||||
raw, err := postJSON(ctx, client, loginURL, payload)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
if loginOK(raw) {
|
||||
return nil
|
||||
}
|
||||
lastErr = fmt.Errorf("kingdee login failed: %s", summarizeRemoteMessage(raw))
|
||||
}
|
||||
if lastErr == nil {
|
||||
lastErr = errors.New("kingdee login failed")
|
||||
}
|
||||
return lastErr
|
||||
}
|
||||
|
||||
func (k *Connector) executeBillQuery(ctx context.Context, client *http.Client, req connectorcontracts.QueryRequest) ([]map[string]any, error) {
|
||||
queryURL := sanitizeKingdeeBaseURL(k.cfg.KingdeeBaseURL) + "Kingdee.BOS.WebApi.ServicesStub.DynamicFormService.ExecuteBillQuery.common.kdsvc"
|
||||
payload := map[string]any{
|
||||
"FormId": req.FormID,
|
||||
"FieldKeys": strings.Join(req.FieldKeys, ","),
|
||||
"FilterString": strings.TrimSpace(req.FilterString),
|
||||
"OrderString": strings.TrimSpace(req.OrderString),
|
||||
"TopRowCount": 0,
|
||||
"StartRow": req.StartRow,
|
||||
"Limit": req.Limit,
|
||||
"SubSystemId": "",
|
||||
}
|
||||
|
||||
raw, err := postJSON(ctx, client, queryURL, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if msg := extractRemoteError(raw); msg != "" {
|
||||
return nil, errors.New(msg)
|
||||
}
|
||||
|
||||
var parsed any
|
||||
if err := json.Unmarshal(raw, &parsed); err != nil {
|
||||
return nil, fmt.Errorf("parse kingdee query response: %w", err)
|
||||
}
|
||||
|
||||
rows := normalizeKingdeeRows(parsed, req.FieldKeys)
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func (k *Connector) resolveObject(objectKey string, formID string) (connectorcontracts.ObjectDefinition, error) {
|
||||
objectKey = strings.TrimSpace(objectKey)
|
||||
if objectKey != "" {
|
||||
if objectDef, ok := kingdeeObjectCatalog[objectKey]; ok {
|
||||
return objectDef, nil
|
||||
}
|
||||
return connectorcontracts.ObjectDefinition{}, fmt.Errorf("unsupported kingdee object: %s", objectKey)
|
||||
}
|
||||
if strings.TrimSpace(formID) == "" {
|
||||
return connectorcontracts.ObjectDefinition{}, errors.New("object_key 或 form_id 至少填写一个")
|
||||
}
|
||||
return connectorcontracts.ObjectDefinition{
|
||||
Key: "custom_query",
|
||||
Label: "自定义表单查询",
|
||||
Description: "通过自定义 FormId 执行 ExecuteBillQuery",
|
||||
Mode: "read",
|
||||
RecommendedFormID: strings.TrimSpace(formID),
|
||||
DefaultFields: []string{},
|
||||
FilterHint: "例如:FDocumentStatus = 'C'",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (k *Connector) isConfigured() bool {
|
||||
return strings.TrimSpace(k.cfg.KingdeeBaseURL) != "" &&
|
||||
strings.TrimSpace(k.cfg.KingdeeAccountID) != "" &&
|
||||
strings.TrimSpace(k.cfg.KingdeeUsername) != "" &&
|
||||
strings.TrimSpace(k.cfg.KingdeePassword) != ""
|
||||
}
|
||||
|
||||
func sanitizeKingdeeBaseURL(base string) string {
|
||||
base = strings.TrimSpace(base)
|
||||
if base == "" {
|
||||
return ""
|
||||
}
|
||||
base = strings.TrimRight(base, "/")
|
||||
if !strings.Contains(strings.ToLower(base), "/k3cloud") {
|
||||
base += "/K3Cloud"
|
||||
}
|
||||
return strings.TrimRight(base, "/") + "/"
|
||||
}
|
||||
|
||||
func postJSON(ctx context.Context, client *http.Client, url string, payload any) ([]byte, error) {
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("http %d: %s", resp.StatusCode, strings.TrimSpace(string(data)))
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func loginOK(raw []byte) bool {
|
||||
var parsed any
|
||||
if err := json.Unmarshal(raw, &parsed); err != nil {
|
||||
return false
|
||||
}
|
||||
switch node := parsed.(type) {
|
||||
case map[string]any:
|
||||
if value, ok := node["LoginResultType"].(float64); ok && int(value) == 1 {
|
||||
return true
|
||||
}
|
||||
if result, ok := node["Result"].(map[string]any); ok {
|
||||
if value, ok := result["LoginResultType"].(float64); ok && int(value) == 1 {
|
||||
return true
|
||||
}
|
||||
if responseStatus, ok := result["ResponseStatus"].(map[string]any); ok {
|
||||
if success, ok := responseStatus["IsSuccess"].(bool); ok && success {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
text := strings.ToLower(string(raw))
|
||||
return strings.Contains(text, `"issuccess":true`) ||
|
||||
strings.Contains(text, `"issuccessbyapi":true`) ||
|
||||
strings.Contains(text, `"loginresulttype":1`)
|
||||
}
|
||||
|
||||
func extractRemoteError(raw []byte) string {
|
||||
var parsed any
|
||||
if err := json.Unmarshal(raw, &parsed); err != nil {
|
||||
return ""
|
||||
}
|
||||
return extractErrorFromParsed(parsed)
|
||||
}
|
||||
|
||||
func extractErrorFromParsed(parsed any) string {
|
||||
switch node := parsed.(type) {
|
||||
case map[string]any:
|
||||
if result, ok := node["Result"].(map[string]any); ok {
|
||||
if responseStatus, ok := result["ResponseStatus"].(map[string]any); ok {
|
||||
if success, ok := responseStatus["IsSuccess"].(bool); ok && !success {
|
||||
if errorsList, ok := responseStatus["Errors"].([]any); ok && len(errorsList) > 0 {
|
||||
messages := make([]string, 0, len(errorsList))
|
||||
for _, item := range errorsList {
|
||||
if msg, ok := item.(map[string]any)["Message"].(string); ok && strings.TrimSpace(msg) != "" {
|
||||
messages = append(messages, strings.TrimSpace(msg))
|
||||
}
|
||||
}
|
||||
if len(messages) > 0 {
|
||||
return strings.Join(messages, "; ")
|
||||
}
|
||||
}
|
||||
return "kingdee response reports failure"
|
||||
}
|
||||
}
|
||||
}
|
||||
if msg, ok := node["Message"].(string); ok && strings.TrimSpace(msg) != "" {
|
||||
return strings.TrimSpace(msg)
|
||||
}
|
||||
}
|
||||
text := strings.TrimSpace(stringMustJSON(parsed))
|
||||
if strings.Contains(text, "会话信息已丢失") {
|
||||
return "会话信息已丢失,请重新登录"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func summarizeRemoteMessage(raw []byte) string {
|
||||
if msg := extractRemoteError(raw); msg != "" {
|
||||
return msg
|
||||
}
|
||||
return strings.TrimSpace(string(raw))
|
||||
}
|
||||
|
||||
func normalizeKingdeeRows(parsed any, fieldKeys []string) []map[string]any {
|
||||
source := parsed
|
||||
if root, ok := parsed.(map[string]any); ok {
|
||||
if result, exists := root["Result"]; exists {
|
||||
source = result
|
||||
}
|
||||
}
|
||||
|
||||
switch node := source.(type) {
|
||||
case []any:
|
||||
rows := make([]map[string]any, 0, len(node))
|
||||
for idx, item := range node {
|
||||
switch row := item.(type) {
|
||||
case []any:
|
||||
if idx == 0 && isHeaderRow(row, fieldKeys) {
|
||||
continue
|
||||
}
|
||||
rows = append(rows, zipRow(fieldKeys, row))
|
||||
case map[string]any:
|
||||
rows = append(rows, row)
|
||||
}
|
||||
}
|
||||
return rows
|
||||
case map[string]any:
|
||||
return []map[string]any{node}
|
||||
default:
|
||||
return []map[string]any{}
|
||||
}
|
||||
}
|
||||
|
||||
func isHeaderRow(row []any, fieldKeys []string) bool {
|
||||
if len(row) != len(fieldKeys) || len(fieldKeys) == 0 {
|
||||
return false
|
||||
}
|
||||
for i := range row {
|
||||
value, ok := row[i].(string)
|
||||
if !ok || strings.TrimSpace(value) != fieldKeys[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func zipRow(fieldKeys []string, values []any) map[string]any {
|
||||
row := make(map[string]any, len(values))
|
||||
for idx, value := range values {
|
||||
key := fmt.Sprintf("col_%d", idx+1)
|
||||
if idx < len(fieldKeys) && strings.TrimSpace(fieldKeys[idx]) != "" {
|
||||
key = fieldKeys[idx]
|
||||
}
|
||||
row[key] = value
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
||||
func stringMustJSON(v any) string {
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
var kingdeeObjectOrder = []string{
|
||||
"customer",
|
||||
"supplier",
|
||||
"material",
|
||||
"sales_order",
|
||||
"purchase_order",
|
||||
"inventory",
|
||||
"receivable",
|
||||
}
|
||||
|
||||
var kingdeeObjectCatalog = map[string]connectorcontracts.ObjectDefinition{
|
||||
"customer": {
|
||||
Key: "customer",
|
||||
Label: "客户主数据",
|
||||
Description: "读取金蝶客户基础资料",
|
||||
Mode: "read",
|
||||
RecommendedFormID: "BD_Customer",
|
||||
DefaultFields: []string{"FNumber", "FName", "FUseOrgId.FNumber", "FCreateOrgId.FNumber", "FDocumentStatus"},
|
||||
FilterHint: "例如:FDocumentStatus = 'C'",
|
||||
},
|
||||
"supplier": {
|
||||
Key: "supplier",
|
||||
Label: "供应商主数据",
|
||||
Description: "读取金蝶供应商基础资料",
|
||||
Mode: "read",
|
||||
RecommendedFormID: "BD_Supplier",
|
||||
DefaultFields: []string{"FNumber", "FName", "FUseOrgId.FNumber", "FCreateOrgId.FNumber", "FDocumentStatus"},
|
||||
FilterHint: "例如:FForbidStatus = 'A'",
|
||||
},
|
||||
"material": {
|
||||
Key: "material",
|
||||
Label: "物料主数据",
|
||||
Description: "读取金蝶物料基础资料",
|
||||
Mode: "read",
|
||||
RecommendedFormID: "BD_MATERIAL",
|
||||
DefaultFields: []string{"FNumber", "FName", "FSpecification", "FBaseUnitId.FNumber", "FDocumentStatus"},
|
||||
FilterHint: "例如:FDocumentStatus = 'C'",
|
||||
},
|
||||
"sales_order": {
|
||||
Key: "sales_order",
|
||||
Label: "销售订单",
|
||||
Description: "读取金蝶销售订单头信息",
|
||||
Mode: "read",
|
||||
RecommendedFormID: "SAL_SaleOrder",
|
||||
DefaultFields: []string{"FBillNo", "FDate", "FCustId.FNumber", "FSaleOrgId.FNumber", "FDocumentStatus"},
|
||||
FilterHint: "例如:FApproveDate >= '2026-08-01'",
|
||||
},
|
||||
"purchase_order": {
|
||||
Key: "purchase_order",
|
||||
Label: "采购订单",
|
||||
Description: "读取金蝶采购订单头信息",
|
||||
Mode: "read",
|
||||
RecommendedFormID: "PUR_PurchaseOrder",
|
||||
DefaultFields: []string{"FBillNo", "FDate", "FSupplierId.FNumber", "FPurchaseOrgId.FNumber", "FDocumentStatus"},
|
||||
FilterHint: "例如:FDocumentStatus = 'C'",
|
||||
},
|
||||
"inventory": {
|
||||
Key: "inventory",
|
||||
Label: "即时库存",
|
||||
Description: "读取金蝶库存对象",
|
||||
Mode: "read",
|
||||
RecommendedFormID: "STK_Inventory",
|
||||
DefaultFields: []string{"FMaterialId.FNumber", "FBaseQty", "FStockId.FNumber", "FOwnerId.FNumber"},
|
||||
FilterHint: "例如:FBaseQty > 0",
|
||||
},
|
||||
"receivable": {
|
||||
Key: "receivable",
|
||||
Label: "应收单",
|
||||
Description: "读取金蝶应收单对象",
|
||||
Mode: "read",
|
||||
RecommendedFormID: "AR_receivable",
|
||||
DefaultFields: []string{"FBillNo", "FDate", "FCUSTOMERID.FNumber", "FSALEORGID.FNumber", "FDOCUMENTSTATUS"},
|
||||
FilterHint: "例如:FDOCUMENTSTATUS = 'C'",
|
||||
},
|
||||
}
|
||||
|
||||
var kingdeeDemoData = map[string][]map[string]any{
|
||||
"customer": {
|
||||
{"FNumber": "CUST0001", "FName": "华北渠道中心", "FUseOrgId.FNumber": "100", "FCreateOrgId.FNumber": "100", "FDocumentStatus": "C"},
|
||||
{"FNumber": "CUST0002", "FName": "博昇样板客户", "FUseOrgId.FNumber": "100", "FCreateOrgId.FNumber": "100", "FDocumentStatus": "C"},
|
||||
},
|
||||
"supplier": {
|
||||
{"FNumber": "SUP0001", "FName": "精密零部件供应商", "FUseOrgId.FNumber": "100", "FCreateOrgId.FNumber": "100", "FDocumentStatus": "C"},
|
||||
{"FNumber": "SUP0002", "FName": "包装材料供应商", "FUseOrgId.FNumber": "100", "FCreateOrgId.FNumber": "100", "FDocumentStatus": "C"},
|
||||
},
|
||||
"material": {
|
||||
{"FNumber": "MAT-1001", "FName": "高强度连接件", "FSpecification": "M8", "FBaseUnitId.FNumber": "PCS", "FDocumentStatus": "C"},
|
||||
{"FNumber": "MAT-1002", "FName": "控制模块", "FSpecification": "CTRL-A", "FBaseUnitId.FNumber": "SET", "FDocumentStatus": "C"},
|
||||
},
|
||||
"sales_order": {
|
||||
{"FBillNo": "SO20260816001", "FDate": "2026-08-16", "FCustId.FNumber": "CUST0001", "FSaleOrgId.FNumber": "100", "FDocumentStatus": "C"},
|
||||
{"FBillNo": "SO20260816002", "FDate": "2026-08-16", "FCustId.FNumber": "CUST0002", "FSaleOrgId.FNumber": "100", "FDocumentStatus": "A"},
|
||||
},
|
||||
"purchase_order": {
|
||||
{"FBillNo": "PO20260816001", "FDate": "2026-08-15", "FSupplierId.FNumber": "SUP0001", "FPurchaseOrgId.FNumber": "100", "FDocumentStatus": "C"},
|
||||
{"FBillNo": "PO20260816002", "FDate": "2026-08-16", "FSupplierId.FNumber": "SUP0002", "FPurchaseOrgId.FNumber": "100", "FDocumentStatus": "B"},
|
||||
},
|
||||
"inventory": {
|
||||
{"FMaterialId.FNumber": "MAT-1001", "FBaseQty": 1820, "FStockId.FNumber": "RAW-01", "FOwnerId.FNumber": "100"},
|
||||
{"FMaterialId.FNumber": "MAT-1002", "FBaseQty": 96, "FStockId.FNumber": "FG-01", "FOwnerId.FNumber": "100"},
|
||||
},
|
||||
"receivable": {
|
||||
{"FBillNo": "AR20260816001", "FDate": "2026-08-12", "FCUSTOMERID.FNumber": "CUST0001", "FSALEORGID.FNumber": "100", "FDOCUMENTSTATUS": "C"},
|
||||
{"FBillNo": "AR20260816002", "FDate": "2026-08-13", "FCUSTOMERID.FNumber": "CUST0002", "FSALEORGID.FNumber": "100", "FDOCUMENTSTATUS": "A"},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package kingdee
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"eai_agentplatform/backend/internal/config"
|
||||
connectorcontracts "eai_agentplatform/backend/internal/connectors/core/contracts"
|
||||
)
|
||||
|
||||
func TestDefinitionDefaultsToDemo(t *testing.T) {
|
||||
definition := New(&config.Config{}).Definition()
|
||||
if definition.Key != "kingdee" {
|
||||
t.Fatalf("unexpected key: %s", definition.Key)
|
||||
}
|
||||
if definition.Direction != "input" {
|
||||
t.Fatalf("expected input direction, got %s", definition.Direction)
|
||||
}
|
||||
if definition.Mode != "demo" {
|
||||
t.Fatalf("expected demo mode, got %s", definition.Mode)
|
||||
}
|
||||
if len(definition.Objects) == 0 {
|
||||
t.Fatal("expected predefined objects")
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryDemoSelectsFields(t *testing.T) {
|
||||
result, err := New(&config.Config{}).Query(context.Background(), connectorcontracts.QueryRequest{
|
||||
ObjectKey: "sales_order",
|
||||
FieldKeys: []string{"FBillNo", "FCustId.FNumber"},
|
||||
Limit: 1,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("query failed: %v", err)
|
||||
}
|
||||
if result.Mode != "demo" {
|
||||
t.Fatalf("expected demo mode, got %s", result.Mode)
|
||||
}
|
||||
|
||||
records, ok := result.Outputs["records"].([]map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("records type mismatch: %T", result.Outputs["records"])
|
||||
}
|
||||
if len(records) != 1 {
|
||||
t.Fatalf("expected 1 record, got %d", len(records))
|
||||
}
|
||||
if _, exists := records[0]["FBillNo"]; !exists {
|
||||
t.Fatal("expected FBillNo field")
|
||||
}
|
||||
if _, exists := records[0]["FCustId.FNumber"]; !exists {
|
||||
t.Fatal("expected FCustId.FNumber field")
|
||||
}
|
||||
if _, exists := records[0]["FDate"]; exists {
|
||||
t.Fatal("did not expect FDate field in selected output")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package connectorquery
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
connectorcontracts "eai_agentplatform/backend/internal/connectors/core/contracts"
|
||||
)
|
||||
|
||||
func FirstNonEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
value = strings.TrimSpace(value)
|
||||
if value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func NormalizeFieldKeys(fieldKeys []string, defaults []string) []string {
|
||||
normalized := make([]string, 0, len(fieldKeys))
|
||||
for _, item := range fieldKeys {
|
||||
item = strings.TrimSpace(item)
|
||||
if item != "" {
|
||||
normalized = append(normalized, item)
|
||||
}
|
||||
}
|
||||
if len(normalized) == 0 {
|
||||
return append([]string{}, defaults...)
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
func NormalizeLimit(limit int) int {
|
||||
switch {
|
||||
case limit <= 0:
|
||||
return 20
|
||||
case limit > 2000:
|
||||
return 2000
|
||||
default:
|
||||
return limit
|
||||
}
|
||||
}
|
||||
|
||||
func BuildRequestEcho(req connectorcontracts.QueryRequest) map[string]any {
|
||||
return map[string]any{
|
||||
"object_key": req.ObjectKey,
|
||||
"form_id": req.FormID,
|
||||
"field_keys": req.FieldKeys,
|
||||
"filter_string": req.FilterString,
|
||||
"order_string": req.OrderString,
|
||||
"start_row": req.StartRow,
|
||||
"limit": req.Limit,
|
||||
"use_demo": req.UseDemo,
|
||||
}
|
||||
}
|
||||
|
||||
func CloneRows(rows []map[string]any) []map[string]any {
|
||||
cloned := make([]map[string]any, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
copyRow := make(map[string]any, len(row))
|
||||
for key, value := range row {
|
||||
copyRow[key] = value
|
||||
}
|
||||
cloned = append(cloned, copyRow)
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
func SliceRows(rows []map[string]any, startRow int, limit int) []map[string]any {
|
||||
if startRow >= len(rows) {
|
||||
return []map[string]any{}
|
||||
}
|
||||
end := startRow + limit
|
||||
if end > len(rows) {
|
||||
end = len(rows)
|
||||
}
|
||||
return rows[startRow:end]
|
||||
}
|
||||
|
||||
func SelectFields(rows []map[string]any, fieldKeys []string) []map[string]any {
|
||||
selected := make([]map[string]any, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
item := make(map[string]any, len(fieldKeys))
|
||||
for _, fieldKey := range fieldKeys {
|
||||
item[fieldKey] = row[fieldKey]
|
||||
}
|
||||
selected = append(selected, item)
|
||||
}
|
||||
return selected
|
||||
}
|
||||
@@ -0,0 +1,406 @@
|
||||
package connectorregistry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"eai_agentplatform/backend/internal/config"
|
||||
connectorcontracts "eai_agentplatform/backend/internal/connectors/core/contracts"
|
||||
"eai_agentplatform/backend/internal/connectors/packages/kingdee"
|
||||
"eai_agentplatform/backend/internal/connectors/query"
|
||||
)
|
||||
|
||||
var connectorOrder = []string{
|
||||
"kingdee",
|
||||
"wms_input",
|
||||
"mes_input",
|
||||
"mail163_input",
|
||||
"dingtalk_table_input",
|
||||
"feishu_bitable_input",
|
||||
"wecom_sheet_input",
|
||||
"dingtalk_table_output",
|
||||
"feishu_bitable_output",
|
||||
"wecom_sheet_output",
|
||||
}
|
||||
|
||||
// ListDefinitions 返回按稳定顺序排序的全部连接器定义。
|
||||
func ListDefinitions(cfg *config.Config) []connectorcontracts.Definition {
|
||||
definitions := registry(cfg)
|
||||
items := make([]connectorcontracts.Definition, 0, len(connectorOrder))
|
||||
for _, key := range connectorOrder {
|
||||
if item, ok := definitions[key]; ok {
|
||||
items = append(items, item)
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
// GetDefinition 按键返回单个连接器定义。
|
||||
func GetDefinition(cfg *config.Config, key string) (connectorcontracts.Definition, bool) {
|
||||
key = strings.TrimSpace(strings.ToLower(key))
|
||||
item, ok := registry(cfg)[key]
|
||||
return item, ok
|
||||
}
|
||||
|
||||
// Query 按连接器键派发查询。
|
||||
func Query(ctx context.Context, cfg *config.Config, key string, req connectorcontracts.QueryRequest) (connectorcontracts.QueryResult, error) {
|
||||
key = strings.TrimSpace(strings.ToLower(key))
|
||||
switch key {
|
||||
case "kingdee":
|
||||
return kingdee.New(cfg).Query(ctx, req)
|
||||
case "wms_input", "mes_input", "mail163_input", "dingtalk_table_input", "feishu_bitable_input", "wecom_sheet_input":
|
||||
return queryStaticConnector(key, req)
|
||||
default:
|
||||
return connectorcontracts.QueryResult{}, connectorcontracts.ErrConnectorNotFound
|
||||
}
|
||||
}
|
||||
|
||||
func registry(cfg *config.Config) map[string]connectorcontracts.Definition {
|
||||
items := map[string]connectorcontracts.Definition{
|
||||
"kingdee": kingdee.New(cfg).Definition(),
|
||||
"wms_input": staticDefinitions["wms_input"],
|
||||
"mes_input": staticDefinitions["mes_input"],
|
||||
"mail163_input": staticDefinitions["mail163_input"],
|
||||
"dingtalk_table_input": staticDefinitions["dingtalk_table_input"],
|
||||
"feishu_bitable_input": staticDefinitions["feishu_bitable_input"],
|
||||
"wecom_sheet_input": staticDefinitions["wecom_sheet_input"],
|
||||
"dingtalk_table_output": staticDefinitions["dingtalk_table_output"],
|
||||
"feishu_bitable_output": staticDefinitions["feishu_bitable_output"],
|
||||
"wecom_sheet_output": staticDefinitions["wecom_sheet_output"],
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func queryStaticConnector(key string, req connectorcontracts.QueryRequest) (connectorcontracts.QueryResult, error) {
|
||||
definition, ok := staticDefinitions[key]
|
||||
if !ok {
|
||||
return connectorcontracts.QueryResult{}, connectorcontracts.ErrConnectorNotFound
|
||||
}
|
||||
if definition.Direction != "input" {
|
||||
return connectorcontracts.QueryResult{}, connectorcontracts.ErrConnectorQueryUnsupported
|
||||
}
|
||||
objectDef, err := resolveStaticObject(definition, req.ObjectKey)
|
||||
if err != nil {
|
||||
return connectorcontracts.QueryResult{}, err
|
||||
}
|
||||
|
||||
req.ObjectKey = objectDef.Key
|
||||
req.FieldKeys = connectorquery.NormalizeFieldKeys(req.FieldKeys, objectDef.DefaultFields)
|
||||
req.Limit = connectorquery.NormalizeLimit(req.Limit)
|
||||
if req.StartRow < 0 {
|
||||
req.StartRow = 0
|
||||
}
|
||||
|
||||
sourceRows := connectorquery.CloneRows(staticDemoRows[key][objectDef.Key])
|
||||
records := connectorquery.SliceRows(sourceRows, req.StartRow, req.Limit)
|
||||
if len(req.FieldKeys) > 0 {
|
||||
records = connectorquery.SelectFields(records, req.FieldKeys)
|
||||
}
|
||||
|
||||
return connectorcontracts.QueryResult{
|
||||
ConnectorKey: definition.Key,
|
||||
ConnectorLabel: definition.Label,
|
||||
ObjectKey: objectDef.Key,
|
||||
ObjectLabel: objectDef.Label,
|
||||
Mode: definition.Mode,
|
||||
Request: connectorquery.BuildRequestEcho(req),
|
||||
Outputs: map[string]any{
|
||||
"count": len(records),
|
||||
"total": len(sourceRows),
|
||||
"records": records,
|
||||
},
|
||||
Artifacts: []map[string]any{},
|
||||
Citations: []map[string]any{
|
||||
{
|
||||
"title": fmt.Sprintf("%s 演示对象 %s", definition.Label, objectDef.Label),
|
||||
"source": "builtin_demo_dataset",
|
||||
"connector": definition.Key,
|
||||
"fetched_at": time.Now().Format(time.RFC3339),
|
||||
},
|
||||
},
|
||||
Risks: []map[string]any{
|
||||
{
|
||||
"level": "low",
|
||||
"code": "demo_mode",
|
||||
"message": "当前连接器为演示模式,尚未绑定真实系统实例。",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func resolveStaticObject(definition connectorcontracts.Definition, objectKey string) (connectorcontracts.ObjectDefinition, error) {
|
||||
objectKey = strings.TrimSpace(objectKey)
|
||||
if objectKey == "" {
|
||||
if len(definition.Objects) == 0 {
|
||||
return connectorcontracts.ObjectDefinition{}, errors.New("当前连接器未定义对象")
|
||||
}
|
||||
return definition.Objects[0], nil
|
||||
}
|
||||
for _, item := range definition.Objects {
|
||||
if item.Key == objectKey {
|
||||
return item, nil
|
||||
}
|
||||
}
|
||||
return connectorcontracts.ObjectDefinition{}, fmt.Errorf("unsupported object for connector %s: %s", definition.Key, objectKey)
|
||||
}
|
||||
|
||||
var staticDefinitions = map[string]connectorcontracts.Definition{
|
||||
"wms_input": {
|
||||
Key: "wms_input",
|
||||
Label: "WMS 输入连接器",
|
||||
Vendor: "Generic",
|
||||
Category: "wms",
|
||||
Direction: "input",
|
||||
Description: "面向仓储业务读取库存、入库、出库等仓储执行数据。",
|
||||
Mode: "demo",
|
||||
Status: "blueprint",
|
||||
AuthConfigured: false,
|
||||
Capabilities: []string{"read", "query", "inventory"},
|
||||
Actions: []connectorcontracts.ActionDefinition{
|
||||
{Key: "fetch_inventory_snapshot", Label: "读取库存快照", ActionType: "FetchInventorySnapshot", Description: "读取仓位库存与可用库存"},
|
||||
{Key: "fetch_inbound_notice", Label: "读取入库通知", ActionType: "FetchInboundNotice", Description: "读取待入库单据"},
|
||||
{Key: "fetch_outbound_task", Label: "读取出库任务", ActionType: "FetchOutboundTask", Description: "读取待执行出库任务"},
|
||||
},
|
||||
Objects: []connectorcontracts.ObjectDefinition{
|
||||
{Key: "inventory_snapshot", Label: "库存快照", Description: "按仓位读取库存记录", Mode: "read", RecommendedFormID: "inventory_snapshot", DefaultFields: []string{"warehouse_code", "location_code", "sku", "on_hand_qty", "available_qty"}, FilterHint: "例如:available_qty > 0"},
|
||||
{Key: "inbound_notice", Label: "入库通知", Description: "读取待收货单据", Mode: "read", RecommendedFormID: "inbound_notice", DefaultFields: []string{"notice_no", "supplier_name", "warehouse_code", "eta_date", "status"}, FilterHint: "例如:status = 'pending'"},
|
||||
{Key: "outbound_task", Label: "出库任务", Description: "读取拣货与发运任务", Mode: "read", RecommendedFormID: "outbound_task", DefaultFields: []string{"task_no", "order_no", "warehouse_code", "pick_status", "carrier"}, FilterHint: "例如:pick_status != 'done'"},
|
||||
},
|
||||
},
|
||||
"mes_input": {
|
||||
Key: "mes_input",
|
||||
Label: "MES 输入连接器",
|
||||
Vendor: "Generic",
|
||||
Category: "mes",
|
||||
Direction: "input",
|
||||
Description: "面向制造执行读取工单、设备告警、报工与质量数据。",
|
||||
Mode: "demo",
|
||||
Status: "blueprint",
|
||||
AuthConfigured: false,
|
||||
Capabilities: []string{"read", "query", "production"},
|
||||
Actions: []connectorcontracts.ActionDefinition{
|
||||
{Key: "fetch_work_order", Label: "读取工单", ActionType: "FetchWorkOrder", Description: "读取生产工单及执行状态"},
|
||||
{Key: "fetch_equipment_alarm", Label: "读取设备告警", ActionType: "FetchEquipmentAlarm", Description: "读取产线设备异常告警"},
|
||||
{Key: "fetch_production_report", Label: "读取生产报工", ActionType: "FetchProductionReport", Description: "读取报工和产量数据"},
|
||||
},
|
||||
Objects: []connectorcontracts.ObjectDefinition{
|
||||
{Key: "work_order", Label: "生产工单", Description: "读取工单状态和排产信息", Mode: "read", RecommendedFormID: "work_order", DefaultFields: []string{"work_order_no", "product_code", "line_code", "planned_qty", "status"}, FilterHint: "例如:status in ('released','running')"},
|
||||
{Key: "equipment_alarm", Label: "设备告警", Description: "读取产线设备告警信息", Mode: "read", RecommendedFormID: "equipment_alarm", DefaultFields: []string{"alarm_no", "equipment_code", "alarm_level", "alarm_time", "status"}, FilterHint: "例如:status = 'open'"},
|
||||
{Key: "production_report", Label: "生产报工", Description: "读取班次报工与产量统计", Mode: "read", RecommendedFormID: "production_report", DefaultFields: []string{"report_no", "work_order_no", "reported_qty", "qualified_qty", "shift_name"}, FilterHint: "例如:reported_qty > 0"},
|
||||
},
|
||||
},
|
||||
"mail163_input": {
|
||||
Key: "mail163_input",
|
||||
Label: "163 邮件输入连接器",
|
||||
Vendor: "NetEase",
|
||||
Category: "email",
|
||||
Direction: "input",
|
||||
Description: "通过 IMAP 读取 163 邮箱中的邮件主题、正文摘要、附件与发件人。",
|
||||
Mode: "demo",
|
||||
Status: "blueprint",
|
||||
AuthConfigured: false,
|
||||
Capabilities: []string{"read", "imap", "attachment"},
|
||||
Actions: []connectorcontracts.ActionDefinition{
|
||||
{Key: "fetch_inbox_mail", Label: "读取收件箱", ActionType: "FetchInboxMail", Description: "拉取收件箱邮件列表"},
|
||||
{Key: "fetch_unread_mail", Label: "读取未读邮件", ActionType: "FetchUnreadMail", Description: "读取未读邮件与摘要"},
|
||||
{Key: "fetch_mail_attachment", Label: "读取邮件附件", ActionType: "FetchMailAttachment", Description: "拉取指定邮件的附件元数据"},
|
||||
},
|
||||
Objects: []connectorcontracts.ObjectDefinition{
|
||||
{Key: "inbox_message", Label: "收件箱邮件", Description: "读取收件箱邮件列表", Mode: "read", RecommendedFormID: "INBOX", DefaultFields: []string{"message_id", "from", "subject", "received_at", "has_attachment"}, FilterHint: "例如:UNSEEN"},
|
||||
{Key: "unread_message", Label: "未读邮件", Description: "读取未读邮件和优先级", Mode: "read", RecommendedFormID: "UNSEEN", DefaultFields: []string{"message_id", "from", "subject", "priority", "received_at"}, FilterHint: "例如:SINCE 16-Aug-2026"},
|
||||
{Key: "mail_attachment", Label: "邮件附件", Description: "读取邮件附件元数据", Mode: "read", RecommendedFormID: "ATTACHMENT", DefaultFields: []string{"message_id", "attachment_name", "attachment_size", "content_type"}, FilterHint: "例如:HASATTACHMENT"},
|
||||
},
|
||||
},
|
||||
"dingtalk_table_input": {
|
||||
Key: "dingtalk_table_input",
|
||||
Label: "钉钉表格输入连接器",
|
||||
Vendor: "DingTalk",
|
||||
Category: "ai_table",
|
||||
Direction: "input",
|
||||
Description: "从钉钉 AI 表格读取 Base、Sheet、Field、Record 结构化数据。",
|
||||
Mode: "demo",
|
||||
Status: "blueprint",
|
||||
AuthConfigured: false,
|
||||
Capabilities: []string{"read", "record", "sheet"},
|
||||
Actions: []connectorcontracts.ActionDefinition{
|
||||
{Key: "list_table_records", Label: "读取表格记录", ActionType: "ListTableRecords", Description: "读取指定表中的多行记录"},
|
||||
{Key: "get_table_schema", Label: "读取表结构", ActionType: "GetTableSchema", Description: "读取字段定义与主字段"},
|
||||
},
|
||||
Objects: []connectorcontracts.ObjectDefinition{
|
||||
{Key: "sheet_record", Label: "表格记录", Description: "读取 AI 表格记录", Mode: "read", RecommendedFormID: "sheet_record", DefaultFields: []string{"record_id", "sheet_name", "title", "owner", "status"}, FilterHint: "例如:status = '进行中'"},
|
||||
{Key: "field_schema", Label: "字段结构", Description: "读取 AI 表格字段结构", Mode: "read", RecommendedFormID: "field_schema", DefaultFields: []string{"field_id", "field_name", "field_type", "required"}, FilterHint: "例如:field_type = 'singleSelect'"},
|
||||
},
|
||||
},
|
||||
"feishu_bitable_input": {
|
||||
Key: "feishu_bitable_input",
|
||||
Label: "飞书表格输入连接器",
|
||||
Vendor: "Feishu",
|
||||
Category: "bitable",
|
||||
Direction: "input",
|
||||
Description: "从飞书多维表格读取记录、视图和字段,适合业务系统型数据输入。",
|
||||
Mode: "demo",
|
||||
Status: "blueprint",
|
||||
AuthConfigured: false,
|
||||
Capabilities: []string{"read", "record", "bitable"},
|
||||
Actions: []connectorcontracts.ActionDefinition{
|
||||
{Key: "list_bitable_records", Label: "读取多维表记录", ActionType: "ListBitableRecords", Description: "读取飞书多维表格记录"},
|
||||
{Key: "get_bitable_fields", Label: "读取字段定义", ActionType: "GetBitableFields", Description: "读取字段配置和类型"},
|
||||
},
|
||||
Objects: []connectorcontracts.ObjectDefinition{
|
||||
{Key: "bitable_record", Label: "多维表记录", Description: "读取业务记录与状态字段", Mode: "read", RecommendedFormID: "bitable_record", DefaultFields: []string{"record_id", "table_name", "title", "owner", "status"}, FilterHint: "例如:status = '待跟进'"},
|
||||
{Key: "field_schema", Label: "字段结构", Description: "读取字段与权限结构", Mode: "read", RecommendedFormID: "field_schema", DefaultFields: []string{"field_id", "field_name", "field_type", "is_primary"}, FilterHint: "例如:field_type = 'Text'"},
|
||||
},
|
||||
},
|
||||
"wecom_sheet_input": {
|
||||
Key: "wecom_sheet_input",
|
||||
Label: "企微表格输入连接器",
|
||||
Vendor: "WeCom",
|
||||
Category: "smart_sheet",
|
||||
Direction: "input",
|
||||
Description: "从企业微信智能表格读取记录、视图和自动化上下文。",
|
||||
Mode: "demo",
|
||||
Status: "blueprint",
|
||||
AuthConfigured: false,
|
||||
Capabilities: []string{"read", "record", "smart_sheet"},
|
||||
Actions: []connectorcontracts.ActionDefinition{
|
||||
{Key: "list_sheet_records", Label: "读取智能表格记录", ActionType: "ListSheetRecords", Description: "读取企微智能表格记录"},
|
||||
{Key: "get_sheet_views", Label: "读取视图定义", ActionType: "GetSheetViews", Description: "读取表格视图和筛选规则"},
|
||||
},
|
||||
Objects: []connectorcontracts.ObjectDefinition{
|
||||
{Key: "sheet_record", Label: "智能表格记录", Description: "读取行级业务记录", Mode: "read", RecommendedFormID: "sheet_record", DefaultFields: []string{"record_id", "sheet_name", "title", "owner", "progress"}, FilterHint: "例如:progress != '已完成'"},
|
||||
{Key: "view_schema", Label: "视图定义", Description: "读取视图与权限信息", Mode: "read", RecommendedFormID: "view_schema", DefaultFields: []string{"view_id", "view_name", "view_type", "permission_scope"}, FilterHint: "例如:view_type = 'kanban'"},
|
||||
},
|
||||
},
|
||||
"dingtalk_table_output": {
|
||||
Key: "dingtalk_table_output",
|
||||
Label: "钉钉表格输出连接器",
|
||||
Vendor: "DingTalk",
|
||||
Category: "ai_table",
|
||||
Direction: "output",
|
||||
Description: "将 DW 结果写入钉钉 AI 表格,适合落库、回填和经营看板。",
|
||||
Mode: "blueprint",
|
||||
Status: "blueprint",
|
||||
AuthConfigured: false,
|
||||
Capabilities: []string{"write", "upsert", "record"},
|
||||
Actions: []connectorcontracts.ActionDefinition{
|
||||
{Key: "insert_table_record", Label: "新增记录", ActionType: "InsertTableRecord", Description: "向钉钉 AI 表格插入记录"},
|
||||
{Key: "update_table_record", Label: "更新记录", ActionType: "UpdateTableRecord", Description: "更新现有记录"},
|
||||
{Key: "append_result_report", Label: "追加结果报表", ActionType: "AppendResultReport", Description: "写入报表汇总行"},
|
||||
},
|
||||
Objects: []connectorcontracts.ObjectDefinition{
|
||||
{Key: "sheet_record", Label: "表格记录", Description: "写入业务结果记录", Mode: "write", RecommendedFormID: "sheet_record", DefaultFields: []string{"title", "owner", "status", "updated_at"}, FilterHint: "按主键 upsert"},
|
||||
},
|
||||
},
|
||||
"feishu_bitable_output": {
|
||||
Key: "feishu_bitable_output",
|
||||
Label: "飞书表格输出连接器",
|
||||
Vendor: "Feishu",
|
||||
Category: "bitable",
|
||||
Direction: "output",
|
||||
Description: "将 DW 结果写入飞书多维表格,适合线索池、任务池和日报沉淀。",
|
||||
Mode: "blueprint",
|
||||
Status: "blueprint",
|
||||
AuthConfigured: false,
|
||||
Capabilities: []string{"write", "upsert", "record"},
|
||||
Actions: []connectorcontracts.ActionDefinition{
|
||||
{Key: "insert_bitable_record", Label: "新增记录", ActionType: "InsertBitableRecord", Description: "向飞书多维表格新增记录"},
|
||||
{Key: "update_bitable_record", Label: "更新记录", ActionType: "UpdateBitableRecord", Description: "更新多维表格记录"},
|
||||
{Key: "sync_result_view", Label: "同步结果视图", ActionType: "SyncResultView", Description: "将结果同步到指定视图"},
|
||||
},
|
||||
Objects: []connectorcontracts.ObjectDefinition{
|
||||
{Key: "bitable_record", Label: "多维表记录", Description: "写入业务结果记录", Mode: "write", RecommendedFormID: "bitable_record", DefaultFields: []string{"title", "owner", "status", "updated_at"}, FilterHint: "按 record_id 或业务主键 upsert"},
|
||||
},
|
||||
},
|
||||
"wecom_sheet_output": {
|
||||
Key: "wecom_sheet_output",
|
||||
Label: "企微表格输出连接器",
|
||||
Vendor: "WeCom",
|
||||
Category: "smart_sheet",
|
||||
Direction: "output",
|
||||
Description: "将 DW 结果写入企业微信智能表格,适合表单回填、项目协同和门店运营。",
|
||||
Mode: "blueprint",
|
||||
Status: "blueprint",
|
||||
AuthConfigured: false,
|
||||
Capabilities: []string{"write", "upsert", "record"},
|
||||
Actions: []connectorcontracts.ActionDefinition{
|
||||
{Key: "insert_sheet_record", Label: "新增记录", ActionType: "InsertSheetRecord", Description: "向企微智能表格新增记录"},
|
||||
{Key: "update_sheet_record", Label: "更新记录", ActionType: "UpdateSheetRecord", Description: "更新已有行"},
|
||||
{Key: "notify_sheet_owner", Label: "通知表格负责人", ActionType: "NotifySheetOwner", Description: "结果写入后通知负责人"},
|
||||
},
|
||||
Objects: []connectorcontracts.ObjectDefinition{
|
||||
{Key: "sheet_record", Label: "智能表格记录", Description: "写入业务结果记录", Mode: "write", RecommendedFormID: "sheet_record", DefaultFields: []string{"title", "owner", "progress", "updated_at"}, FilterHint: "按业务键 upsert"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var staticDemoRows = map[string]map[string][]map[string]any{
|
||||
"wms_input": {
|
||||
"inventory_snapshot": {
|
||||
{"warehouse_code": "WH-SZ-01", "location_code": "A-01-01", "sku": "MAT-1001", "on_hand_qty": 860, "available_qty": 820},
|
||||
{"warehouse_code": "WH-SZ-01", "location_code": "B-03-06", "sku": "MAT-1002", "on_hand_qty": 96, "available_qty": 80},
|
||||
},
|
||||
"inbound_notice": {
|
||||
{"notice_no": "IBN-20260816-001", "supplier_name": "精密零部件供应商", "warehouse_code": "WH-SZ-01", "eta_date": "2026-08-17", "status": "pending"},
|
||||
},
|
||||
"outbound_task": {
|
||||
{"task_no": "OBT-20260816-008", "order_no": "SO20260816001", "warehouse_code": "WH-SZ-01", "pick_status": "picking", "carrier": "SF"},
|
||||
},
|
||||
},
|
||||
"mes_input": {
|
||||
"work_order": {
|
||||
{"work_order_no": "MO-20260816-001", "product_code": "FG-8821", "line_code": "LINE-03", "planned_qty": 1200, "status": "running"},
|
||||
{"work_order_no": "MO-20260816-002", "product_code": "FG-8822", "line_code": "LINE-01", "planned_qty": 500, "status": "released"},
|
||||
},
|
||||
"equipment_alarm": {
|
||||
{"alarm_no": "ALM-03-091", "equipment_code": "SMT-03", "alarm_level": "high", "alarm_time": "2026-08-16T09:26:00+08:00", "status": "open"},
|
||||
},
|
||||
"production_report": {
|
||||
{"report_no": "PR-20260816-03A", "work_order_no": "MO-20260816-001", "reported_qty": 480, "qualified_qty": 468, "shift_name": "白班"},
|
||||
},
|
||||
},
|
||||
"mail163_input": {
|
||||
"inbox_message": {
|
||||
{"message_id": "<msg-1001@163.com>", "from": "buyer-a@example.com", "subject": "本周交期确认", "received_at": "2026-08-16T08:11:00+08:00", "has_attachment": true},
|
||||
{"message_id": "<msg-1002@163.com>", "from": "finance@example.com", "subject": "8月回款对账单", "received_at": "2026-08-16T10:05:00+08:00", "has_attachment": false},
|
||||
},
|
||||
"unread_message": {
|
||||
{"message_id": "<msg-1003@163.com>", "from": "factory@example.com", "subject": "产线停机预警", "priority": "high", "received_at": "2026-08-16T10:21:00+08:00"},
|
||||
},
|
||||
"mail_attachment": {
|
||||
{"message_id": "<msg-1001@163.com>", "attachment_name": "需求清单.xlsx", "attachment_size": 245760, "content_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"},
|
||||
},
|
||||
},
|
||||
"dingtalk_table_input": {
|
||||
"sheet_record": {
|
||||
{"record_id": "rec_dt_001", "sheet_name": "商机跟进", "title": "华北项目推进", "owner": "王敏", "status": "进行中"},
|
||||
{"record_id": "rec_dt_002", "sheet_name": "售后工单", "title": "客户现场异常", "owner": "刘浩", "status": "待处理"},
|
||||
},
|
||||
"field_schema": {
|
||||
{"field_id": "fld_dt_001", "field_name": "负责人", "field_type": "user", "required": true},
|
||||
{"field_id": "fld_dt_002", "field_name": "状态", "field_type": "singleSelect", "required": true},
|
||||
},
|
||||
},
|
||||
"feishu_bitable_input": {
|
||||
"bitable_record": {
|
||||
{"record_id": "rec_fs_001", "table_name": "客户线索池", "title": "深圳制造业客户", "owner": "李晓", "status": "待跟进"},
|
||||
{"record_id": "rec_fs_002", "table_name": "合同审查任务", "title": "框架协议审查", "owner": "陈珂", "status": "审核中"},
|
||||
},
|
||||
"field_schema": {
|
||||
{"field_id": "fld_fs_001", "field_name": "状态", "field_type": "SingleSelect", "is_primary": false},
|
||||
{"field_id": "fld_fs_002", "field_name": "标题", "field_type": "Text", "is_primary": true},
|
||||
},
|
||||
},
|
||||
"wecom_sheet_input": {
|
||||
"sheet_record": {
|
||||
{"record_id": "rec_wc_001", "sheet_name": "门店巡检", "title": "福田门店巡检", "owner": "赵琴", "progress": "待复核"},
|
||||
{"record_id": "rec_wc_002", "sheet_name": "销售日报", "title": "华东区日报", "owner": "郭鹏", "progress": "已提交"},
|
||||
},
|
||||
"view_schema": {
|
||||
{"view_id": "view_wc_001", "view_name": "本周待办", "view_type": "kanban", "permission_scope": "sales_team"},
|
||||
{"view_id": "view_wc_002", "view_name": "门店异常", "view_type": "table", "permission_scope": "ops_team"},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package connectorregistry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"eai_agentplatform/backend/internal/config"
|
||||
connectorcontracts "eai_agentplatform/backend/internal/connectors/core/contracts"
|
||||
)
|
||||
|
||||
func TestListDefinitionsContainsInputAndOutputConnectors(t *testing.T) {
|
||||
definitions := ListDefinitions(&config.Config{})
|
||||
if len(definitions) < 8 {
|
||||
t.Fatalf("expected multiple connectors, got %d", len(definitions))
|
||||
}
|
||||
|
||||
foundInput := false
|
||||
foundOutput := false
|
||||
for _, item := range definitions {
|
||||
if item.Direction == "input" {
|
||||
foundInput = true
|
||||
}
|
||||
if item.Direction == "output" {
|
||||
foundOutput = true
|
||||
}
|
||||
}
|
||||
if !foundInput || !foundOutput {
|
||||
t.Fatalf("expected both input and output connectors, input=%v output=%v", foundInput, foundOutput)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStaticInputConnectorQueryDemo(t *testing.T) {
|
||||
result, err := Query(context.Background(), &config.Config{}, "wecom_sheet_input", connectorcontracts.QueryRequest{
|
||||
ObjectKey: "sheet_record",
|
||||
FieldKeys: []string{"record_id", "title"},
|
||||
Limit: 1,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("query failed: %v", err)
|
||||
}
|
||||
if result.ConnectorKey != "wecom_sheet_input" {
|
||||
t.Fatalf("unexpected connector key: %s", result.ConnectorKey)
|
||||
}
|
||||
if result.ObjectKey != "sheet_record" {
|
||||
t.Fatalf("unexpected object key: %s", result.ObjectKey)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user