feat: expand companion platform, device controls, and memoir workflows
ci / verify (push) Canceled after 0s
ci / verify (push) Canceled after 0s
Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
// Package elderagent provisions one persistent Volcano bot per elder.
|
||||
package elderagent
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
volcbase "github.com/volcengine/volc-sdk-golang/base"
|
||||
)
|
||||
|
||||
type Bot struct {
|
||||
ID string `json:"Id"`
|
||||
Name string `json:"Name"`
|
||||
Config map[string]any `json:"Config"`
|
||||
AgentConfig map[string]any `json:"AgentConfig"`
|
||||
Bindings []map[string]any `json:"Bindings"`
|
||||
}
|
||||
type Provider interface {
|
||||
Ensure(context.Context, string, map[string]any, map[string]any) (Bot, error)
|
||||
}
|
||||
|
||||
func (c *Client) Update(ctx context.Context, id string, config, agent map[string]any) error {
|
||||
var result map[string]any
|
||||
return c.call(ctx, "AibotUpdate", map[string]any{"Id": id, "Config": config, "AgentConfig": agent}, &result)
|
||||
}
|
||||
|
||||
// Verify managed fields with a readback. Credentials can be masked by the
|
||||
// provider and are deliberately neither compared nor returned in diagnostics.
|
||||
func (c *Client) VerifyConfiguration(ctx context.Context, id string, config, agent map[string]any) error {
|
||||
var result struct {
|
||||
Bots []Bot `json:"Bots"`
|
||||
}
|
||||
if err := c.call(ctx, "AibotQuery", map[string]any{"Id": id}, &result); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, bot := range result.Bots {
|
||||
if bot.ID != id {
|
||||
continue
|
||||
}
|
||||
for _, path := range [][]string{{"S2SConfig", "ProviderParams", "dialog", "system_role"}, {"MemoryConfig", "Enable"}} {
|
||||
want, wok := configurationField(config, path)
|
||||
got, gok := configurationField(bot.Config, path)
|
||||
if !wok || !gok || !sameJSON(want, got) {
|
||||
return fmt.Errorf("PERSONA_VERIFY_FAILED")
|
||||
}
|
||||
}
|
||||
for _, key := range []string{"WelcomeMessage", "ServerMessageURLForRTS", "ServerMessageSignatureForRTS"} {
|
||||
want, wok := agent[key]
|
||||
got, gok := bot.AgentConfig[key]
|
||||
if wok && (!gok || !sameJSON(want, got)) {
|
||||
return fmt.Errorf("PERSONA_VERIFY_FAILED")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("PERSONA_VERIFY_FAILED")
|
||||
}
|
||||
|
||||
func configurationField(m map[string]any, path []string) (any, bool) {
|
||||
for _, key := range path[:len(path)-1] {
|
||||
m, _ = m[key].(map[string]any)
|
||||
if m == nil {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
v, ok := m[path[len(path)-1]]
|
||||
return v, ok
|
||||
}
|
||||
func sameJSON(a, b any) bool {
|
||||
ar, _ := json.Marshal(a)
|
||||
br, _ := json.Marshal(b)
|
||||
return bytes.Equal(ar, br)
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
Endpoint string
|
||||
Credentials volcbase.Credentials
|
||||
HTTP *http.Client
|
||||
Binding map[string]any
|
||||
}
|
||||
|
||||
func (c *Client) ForProduct(instance, product string) *Client {
|
||||
if instance != "" && product != "" {
|
||||
c.Binding = map[string]any{"InstanceId": instance, "ProductKey": product}
|
||||
}
|
||||
return c
|
||||
}
|
||||
func (c *Client) bind(ctx context.Context, bot Bot) error {
|
||||
if len(c.Binding) == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, b := range bot.Bindings {
|
||||
if b["InstanceId"] == c.Binding["InstanceId"] && b["ProductKey"] == c.Binding["ProductKey"] {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
var result map[string]any
|
||||
return c.call(ctx, "AibotBind", map[string]any{"Id": bot.ID, "Binding": c.Binding}, &result)
|
||||
}
|
||||
func (c *Client) BindConfigured(ctx context.Context, id string) error {
|
||||
if len(c.Binding) == 0 {
|
||||
return nil
|
||||
}
|
||||
var result struct {
|
||||
Bots []Bot `json:"Bots"`
|
||||
}
|
||||
if err := c.call(ctx, "AibotQuery", map[string]any{"Id": id, "WithBindings": true}, &result); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, bot := range result.Bots {
|
||||
if bot.ID == id {
|
||||
return c.bind(ctx, bot)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("AibotQuery_NOT_FOUND")
|
||||
}
|
||||
|
||||
func NewClient(ak, sk, token, endpoint, region string) *Client {
|
||||
if endpoint == "" {
|
||||
endpoint = "https://rtc.volcengineapi.com"
|
||||
}
|
||||
if region == "" {
|
||||
region = "cn-north-1"
|
||||
}
|
||||
return &Client{Endpoint: endpoint, Credentials: volcbase.Credentials{AccessKeyID: ak, SecretAccessKey: sk, SessionToken: token, Service: "rtc", Region: region}, HTTP: &http.Client{Timeout: 15 * time.Second}}
|
||||
}
|
||||
func (c *Client) call(ctx context.Context, action string, body any, result any) error {
|
||||
if c.Credentials.AccessKeyID == "" || c.Credentials.SecretAccessKey == "" {
|
||||
return fmt.Errorf("VOLC_CREDENTIALS_REQUIRED")
|
||||
}
|
||||
u, err := url.Parse(c.Endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
q := u.Query()
|
||||
q.Set("Action", action)
|
||||
q.Set("Version", "2025-08-01")
|
||||
u.RawQuery = q.Encode()
|
||||
raw, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), bytes.NewReader(raw))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
c.Credentials.Sign(req)
|
||||
res, err := c.HTTP.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s_TRANSPORT_ERROR", action)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
var env struct {
|
||||
ResponseMetadata struct {
|
||||
Error *struct {
|
||||
Code string `json:"Code"`
|
||||
} `json:"Error"`
|
||||
} `json:"ResponseMetadata"`
|
||||
Result json.RawMessage `json:"Result"`
|
||||
}
|
||||
if err = json.NewDecoder(io.LimitReader(res.Body, 4<<20)).Decode(&env); err != nil {
|
||||
return fmt.Errorf("%s_INVALID_RESPONSE", action)
|
||||
}
|
||||
if env.ResponseMetadata.Error != nil {
|
||||
return fmt.Errorf("%s:%s", action, env.ResponseMetadata.Error.Code)
|
||||
}
|
||||
if res.StatusCode < 200 || res.StatusCode >= 300 {
|
||||
return fmt.Errorf("%s_HTTP_%d", action, res.StatusCode)
|
||||
}
|
||||
return json.Unmarshal(env.Result, result)
|
||||
}
|
||||
|
||||
// A deterministic name recovers an already-created bot after a lost response.
|
||||
// Callers serialize this method for the same elder, including across API workers.
|
||||
func (c *Client) Ensure(ctx context.Context, name string, config, agent map[string]any) (Bot, error) {
|
||||
var queried struct {
|
||||
Bots []Bot `json:"Bots"`
|
||||
NextIterator string `json:"NextIterator"`
|
||||
}
|
||||
iterator := ""
|
||||
for {
|
||||
if err := c.call(ctx, "AibotQuery", map[string]any{"Name": name, "AccessType": "public", "WithBindings": true, "Limit": 200, "Iterator": iterator}, &queried); err != nil {
|
||||
return Bot{}, err
|
||||
}
|
||||
for _, b := range queried.Bots {
|
||||
if b.Name == name {
|
||||
var updated map[string]any
|
||||
if err := c.call(ctx, "AibotUpdate", map[string]any{"Id": b.ID, "Config": config, "AgentConfig": agent}, &updated); err != nil {
|
||||
return Bot{}, err
|
||||
}
|
||||
b.Config = config
|
||||
b.AgentConfig = agent
|
||||
if err := c.bind(ctx, b); err != nil {
|
||||
return Bot{}, err
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
}
|
||||
if queried.NextIterator == "" {
|
||||
break
|
||||
}
|
||||
if queried.NextIterator == iterator {
|
||||
return Bot{}, fmt.Errorf("AibotQuery_INVALID_PAGINATION")
|
||||
}
|
||||
iterator = queried.NextIterator
|
||||
}
|
||||
var created struct {
|
||||
Id string `json:"Id"`
|
||||
}
|
||||
if err := c.call(ctx, "AibotCreate", map[string]any{"Name": name, "AccessType": "public", "Config": config, "AgentConfig": agent}, &created); err != nil {
|
||||
return Bot{}, err
|
||||
}
|
||||
if created.Id == "" {
|
||||
return Bot{}, fmt.Errorf("AibotCreate_MISSING_ID")
|
||||
}
|
||||
bot := Bot{ID: created.Id, Name: name, Config: config, AgentConfig: agent}
|
||||
if err := c.bind(ctx, bot); err != nil {
|
||||
return Bot{}, err
|
||||
}
|
||||
return bot, nil
|
||||
}
|
||||
Reference in New Issue
Block a user