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:
eaiadmin
2026-09-19 01:23:51 +08:00
co-authored by Claude Code
parent ddd2d2cbd8
commit 14f303459e
448 changed files with 18792 additions and 17027 deletions
@@ -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)
}
}